/* Pi-hole: A black hole for Internet advertisements * (c) 2017 Pi-hole, LLC (https://pi-hole.net) * Network-wide ad blocking via your own hardware. * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ /* global utils:false, Chart:false, apiFailure:false */ // Define global variables var timeLineChart, clientsChart; var queryTypePieChart, forwardDestinationPieChart; var THEME_COLORS = [ "#f56954", "#3c8dbc", "#00a65a", "#00c0ef", "#f39c12", "#0073b7", "#001f3f", "#39cccc", "#3d9970", "#01ff70", "#ff851b", "#f012be", "#8e24aa", "#d81b60", "#222222", "#d2d6de", ]; var customTooltips = function (context) { var tooltip = context.tooltip; var tooltipEl = document.getElementById(this._chart.canvas.id + "-customTooltip"); if (!tooltipEl) { // Create Tooltip Element once per chart tooltipEl = document.createElement("div"); tooltipEl.id = this._chart.canvas.id + "-customTooltip"; tooltipEl.classList.add("chartjs-tooltip"); tooltipEl.innerHTML = "
"; // avoid browser's font-zoom since we know that 's // font-size was set to 14px by bootstrap's css var fontZoom = parseFloat($("body").css("font-size")) / 14; // set styles and font tooltipEl.style.padding = tooltip.options.padding + "px " + tooltip.options.padding + "px"; tooltipEl.style.borderRadius = tooltip.options.cornerRadius + "px"; tooltipEl.style.font = tooltip.options.bodyFont.string; tooltipEl.style.fontFamily = tooltip.options.bodyFont.family; tooltipEl.style.fontSize = tooltip.options.bodyFont.size / fontZoom + "px"; tooltipEl.style.fontStyle = tooltip.options.bodyFont.style; // append Tooltip next to canvas-containing box tooltipEl.ancestor = this._chart.canvas.closest(".box[id]").parentNode; tooltipEl.ancestor.append(tooltipEl); } // Hide if no tooltip if (tooltip.opacity === 0) { tooltipEl.style.opacity = 0; return; } // Set caret position tooltipEl.classList.remove("left", "right", "center", "top", "bottom"); tooltipEl.classList.add(tooltip.xAlign, tooltip.yAlign); // Set Text if (tooltip.body) { var titleLines = tooltip.title || []; var bodyLines = tooltip.body.map(function (bodyItem) { return bodyItem.lines; }); var innerHtml = ""; titleLines.forEach(function (title) { innerHtml += "" + title + ""; }); innerHtml += ""; var printed = 0; var devicePixel = (1 / window.devicePixelRatio).toFixed(1); bodyLines.forEach(function (body, i) { var labelColors = tooltip.labelColors[i]; var style = "background-color: " + labelColors.backgroundColor; style += "; outline: 1px solid " + labelColors.backgroundColor; style += "; border: " + devicePixel + "px solid #fff"; var span = ""; var num = body[0].split(": "); // do not display entries with value of 0 (in bar chart), // but pass through entries with "0.0% (in pie charts) if (num[1] !== "0") { innerHtml += "" + span + body + ""; printed++; } }); if (printed < 1) { innerHtml += "No activity recorded"; } innerHtml += ""; var tableRoot = tooltipEl.querySelector("table"); tableRoot.innerHTML = innerHtml; } var canvasPos = this._chart.canvas.getBoundingClientRect(); var boxPos = tooltipEl.ancestor.getBoundingClientRect(); var offsetX = canvasPos.left - boxPos.left; var offsetY = canvasPos.top - boxPos.top; var tooltipWidth = tooltipEl.offsetWidth; var tooltipHeight = tooltipEl.offsetHeight; var caretX = tooltip.caretX; var caretY = tooltip.caretY; var caretPadding = tooltip.options.caretPadding; var tooltipX, tooltipY, arrowX; var arrowMinIndent = 2 * tooltip.options.cornerRadius; var arrowSize = 5; // Compute X position if ($(document).width() > 2 * tooltip.width || tooltip.xAlign !== "center") { // If the viewport is wide enough, let the tooltip follow the caret position tooltipX = offsetX + caretX; if (tooltip.yAlign === "top" || tooltip.yAlign === "bottom") { switch (tooltip.xAlign) { case "center": // set a minimal X position to 5px to prevent // the tooltip to stick out left of the viewport var minX = 5; if (2 * tooltipX < tooltipWidth + minX) { arrowX = tooltipX - minX; tooltipX = minX; } else { tooltipX -= tooltipWidth / 2; } break; case "left": tooltipX -= arrowMinIndent; arrowX = arrowMinIndent; break; case "right": tooltipX -= tooltipWidth - arrowMinIndent; arrowX = tooltipWidth - arrowMinIndent; break; default: break; } } else if (tooltip.yAlign === "center") { switch (tooltip.xAlign) { case "left": tooltipX += caretPadding; break; case "right": tooltipX -= tooltipWidth - caretPadding; break; case "center": tooltipX -= tooltipWidth / 2; break; default: break; } } } else { // compute the tooltip's center inside ancestor element tooltipX = (tooltipEl.ancestor.offsetWidth - tooltipWidth) / 2; // move the tooltip if the arrow would stick out to the left if (offsetX + caretX - arrowMinIndent < tooltipX) { tooltipX = offsetX + caretX - arrowMinIndent; } // move the tooltip if the arrow would stick out to the right if (offsetX + caretX - tooltipWidth + arrowMinIndent > tooltipX) { tooltipX = offsetX + caretX - tooltipWidth + arrowMinIndent; } arrowX = offsetX + caretX - tooltipX; } // Compute Y position switch (tooltip.yAlign) { case "top": tooltipY = offsetY + caretY + arrowSize + caretPadding; break; case "center": tooltipY = offsetY + caretY - tooltipHeight / 2; if (tooltip.xAlign === "left") { tooltipX += arrowSize; } else if (tooltip.xAlign === "right") { tooltipX -= arrowSize; } break; case "bottom": tooltipY = offsetY + caretY - tooltipHeight - arrowSize - caretPadding; break; default: break; } // Position tooltip and display tooltipEl.style.top = tooltipY.toFixed(1) + "px"; tooltipEl.style.left = tooltipX.toFixed(1) + "px"; if (arrowX === undefined) { tooltipEl.querySelector(".arrow").style.left = ""; } else { // Calculate percentage X value depending on the tooltip's // width to avoid hanging arrow out on tooltip width changes var arrowXpercent = ((100 / tooltipWidth) * arrowX).toFixed(1); tooltipEl.querySelector(".arrow").style.left = arrowXpercent + "%"; } tooltipEl.style.opacity = 1; }; // Functions to update data in page var failures = 0; function updateQueriesOverTime() { $.getJSON("/api/history", function (data) { // Remove possibly already existing data timeLineChart.data.labels = []; timeLineChart.data.datasets = []; var labels = ["Blocked DNS Queries", "Cached DNS Queries", "Forwarded DNS Queries"]; var cachedColor = utils.getCSSval("queries-cached", "background-color"); var blockedColor = utils.getCSSval("queries-blocked", "background-color"); var permittedColor = utils.getCSSval("queries-permitted", "background-color"); var colors = [blockedColor, cachedColor, permittedColor]; // Collect values and colors, and labels for (var i = 0; i < labels.length; i++) { timeLineChart.data.datasets.push({ data: [], // If we ran out of colors, make a random one backgroundColor: colors[i], pointRadius: 0, pointHitRadius: 5, pointHoverRadius: 5, label: labels[i], cubicInterpolationMode: "monotone", }); } // Add data for each dataset that is available data.history.forEach(function (item) { var timestamp = new Date(1000 * parseInt(item.timestamp, 10)); timeLineChart.data.labels.push(timestamp); var blocked = item.blocked; var cached = item.cached; var permitted = item.total - (blocked + cached); timeLineChart.data.datasets[0].data.push(blocked); timeLineChart.data.datasets[1].data.push(cached); timeLineChart.data.datasets[2].data.push(permitted); }); $("#queries-over-time .overlay").hide(); timeLineChart.update(); }) .done(function () { // Reload graph after 10 minutes failures = 0; setTimeout(updateQueriesOverTime, 600000); }) .fail(function () { failures++; if (failures < 5) { // Try again after 1 minute only if this has not failed more // than five times in a row setTimeout(updateQueriesOverTime, 60000); } }) .fail(function (data) { apiFailure(data); }); } var querytypeids = []; function updateQueryTypesPie() { $.getJSON("/api/stats/query_types", function (data) { var v = [], c = [], k = [], i = 0, sum = 0; // Compute total number of queries Object.keys(data.types).forEach(function (item) { sum += data.types[item]; }); // Fill chart with data (only include query types which appeared recently) querytypeids = []; Object.keys(data.types).forEach(function (item) { if (data.types[item] > 0) { v.push((100 * data.types[item]) / sum); c.push(THEME_COLORS[i % THEME_COLORS.length]); k.push(item); querytypeids.push(i + 1); } i++; }); // Build a single dataset with the data to be pushed var dd = { data: v, backgroundColor: c }; // and push it at once queryTypePieChart.data.datasets[0] = dd; queryTypePieChart.data.labels = k; $("#query-types-pie .overlay").hide(); queryTypePieChart.update(); // Don't use rotation animation for further updates queryTypePieChart.options.animation.duration = 0; }) .done(function () { // Reload graph after minute setTimeout(updateQueryTypesPie, 60000); }) .fail(function (data) { apiFailure(data); }); } function updateClientsOverTime() { $.getJSON("/api/history/clients", function (data) { // Remove graph if there are no results (e.g. new // installation or privacy mode enabled) if (jQuery.isEmptyObject(data.history)) { $("#clients-over-time").parent().remove(); return; } // remove last data point for line charts as it is not representative there if (utils.getGraphType() === "line") data.history.splice(-1, 1); var i, labels = []; data.clients.forEach(function (client) { labels.push(client.name !== null ? client.name : client.ip); }); // Remove possibly already existing data clientsChart.data.labels = []; clientsChart.data.datasets = []; for (i = 0; i < data.clients.length; i++) { clientsChart.data.datasets.push({ data: [], // If we ran out of colors, make a random one backgroundColor: i < THEME_COLORS.length ? THEME_COLORS[i] : "#" + (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6), pointRadius: 0, pointHitRadius: 5, pointHoverRadius: 5, label: labels[i], cubicInterpolationMode: "monotone", }); } // Add data for each dataset that is available data.clients.forEach(function (i, c) { data.history.forEach(function (item) { clientsChart.data.datasets[c].data.push(item.data[c]); }); }); // Extract data timestamps data.history.forEach(function (item) { var d = new Date(1000 * parseInt(item.timestamp, 10)); clientsChart.data.labels.push(d); }); $("#clients .overlay").hide(); clientsChart.update(); }) .done(function () { // Reload graph after 10 minutes failures = 0; setTimeout(updateClientsOverTime, 600000); }) .fail(function () { failures++; if (failures < 5) { // Try again after 1 minute only if this has not failed more // than five times in a row setTimeout(updateClientsOverTime, 60000); } }) .fail(function (data) { apiFailure(data); }); } function updateForwardDestinationsPie() { $.getJSON("/api/stats/upstreams", function (data) { var v = [], c = [], k = [], i = 0, sum = 0, values = []; // Compute total number of queries data.upstreams.forEach(function (item) { sum += item.count; }); // Collect values and colors data.upstreams.forEach(function (item) { var label = item.ip; if (item.name.length > 0) { label = item.ip; } var percent = (100 * item.count) / sum; values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]); }); // Split data into individual arrays for the graphs values.forEach(function (value) { k.push(value[0]); v.push(value[1]); c.push(value[2]); }); // Build a single dataset with the data to be pushed var dd = { data: v, backgroundColor: c }; // and push it at once forwardDestinationPieChart.data.labels = k; forwardDestinationPieChart.data.datasets[0] = dd; // and push it at once $("#forward-destinations-pie .overlay").hide(); forwardDestinationPieChart.update(); // Don't use rotation animation for further updates forwardDestinationPieChart.options.animation.duration = 0; }) .done(function () { // Reload graph after one minute setTimeout(updateForwardDestinationsPie, 60000); }) .fail(function (data) { apiFailure(data); }); } function updateTopClientsTable(blocked) { var api, style, tablecontent, overlay, clienttable; if (blocked) { api = "/api/stats/top_clients?blocked=true"; style = "queries-blocked"; tablecontent = $("#client-frequency-blocked td").parent(); overlay = $("#client-frequency-blocked .overlay"); clienttable = $("#client-frequency-blocked").find("tbody:last"); } else { api = "/api/stats/top_clients"; style = "queries-permitted"; tablecontent = $("#client-frequency td").parent(); overlay = $("#client-frequency .overlay"); clienttable = $("#client-frequency").find("tbody:last"); } $.getJSON(api, function (data) { // Clear tables before filling them with data tablecontent.remove(); var url, percentage, sum = blocked ? data.blocked_queries : data.total_queries; // Add note if there are no results (e.g. privacy mode enabled) if (jQuery.isEmptyObject(data.clients)) { clienttable.append('
- No data -
'); } // Populate table with content data.clients.forEach(function (client) { // Sanitize client var clientname = utils.escapeHtml(client.name); var clientip = utils.escapeHtml(client.ip); if (clientname.length === 0) clientname = clientip; url = '' + clientname + ""; percentage = (client.count / sum) * 100; // Add row to table clienttable.append( " " + utils.addTD(url) + utils.addTD(client.count) + utils.addTD(utils.colorBar(percentage, sum, style)) + " " ); }); // Hide overlay overlay.hide(); }).fail(function (data) { apiFailure(data); }); } function updateTopDomainsTable(blocked) { var api, style, tablecontent, overlay, domaintable; if (blocked) { api = "/api/stats/top_domains?blocked=true"; style = "queries-blocked"; tablecontent = $("#ad-frequency td").parent(); overlay = $("#ad-frequency .overlay"); domaintable = $("#ad-frequency").find("tbody:last"); } else { api = "/api/stats/top_domains"; style = "queries-permitted"; tablecontent = $("#domain-frequency td").parent(); overlay = $("#domain-frequency .overlay"); domaintable = $("#domain-frequency").find("tbody:last"); } $.getJSON(api, function (data) { // Clear tables before filling them with data tablecontent.remove(); var url, domain, percentage, urlText, sum = blocked ? data.blocked_queries : data.total_queries; // Add note if there are no results (e.g. privacy mode enabled) if (jQuery.isEmptyObject(data.domains)) { domaintable.append('
- No data -
'); } // Populate table with content data.domains.forEach(function (item) { // Sanitize domain domain = utils.escapeHtml(item.domain); // Substitute "." for empty domain lookups urlText = domain === "" ? "." : domain; url = '' + urlText + ""; percentage = (item.count / sum) * 100; domaintable.append( " " + utils.addTD(url) + utils.addTD(item.count) + utils.addTD(utils.colorBar(percentage, sum, style)) + " " ); }); overlay.hide(); }).fail(function (data) { apiFailure(data); }); } function updateTopLists() { // Update blocked domains updateTopDomainsTable(true); // Update permitted domains updateTopDomainsTable(false); // Update blocked clients updateTopClientsTable(true); // Update permitted clients updateTopClientsTable(false); // Update top lists data every 10 seconds setTimeout(updateTopLists, 10000); } function glowIfChanged(elem, textData) { if (elem.text() !== textData) { elem.addClass("glow"); elem.text(textData); } } function updateSummaryData(runOnce) { var setTimer = function (timeInSeconds) { if (!runOnce) { setTimeout(updateSummaryData, timeInSeconds * 1000); } }; $.getJSON("/api/stats/summary", function (data) { var intl = new Intl.NumberFormat(); glowIfChanged($("span#dns_queries"), intl.format(parseInt(data.queries.total, 10))); glowIfChanged($("span#total_clients"), intl.format(parseInt(data.clients.total, 10))); glowIfChanged($("span#blocked_queries"), intl.format(parseFloat(data.queries.blocked))); glowIfChanged( $("span#percent_blocked"), parseFloat(data.queries.percent_blocked).toFixed(1) + "%" ); glowIfChanged( $("span#gravity_size"), intl.format(parseInt(data.gravity.domains_being_blocked, 10)) ); setTimeout(function () { $("span.glow").removeClass("glow"); }, 500); }) .done(function () { setTimer(1); }) .fail(function (data) { setTimer(300); apiFailure(data); }); } function doughnutTooltip(tooltipLabel) { var percentageTotalShown = tooltipLabel.chart._metasets[0].total.toFixed(1); // tooltipLabel.chart._metasets[0].total returns the total percentage of the shown slices // to compensate rounding errors we round to one decimal var label = " " + tooltipLabel.label; var itemPercentage; // if we only show < 1% percent of all, show each item with two decimals if (percentageTotalShown < 1) { itemPercentage = tooltipLabel.parsed.toFixed(2); } else { // show with one decimal, but in case the item share is really small it could be rounded to 0.0 // we compensate for this itemPercentage = tooltipLabel.parsed.toFixed(1) === "0.0" ? "< 0.1" : tooltipLabel.parsed.toFixed(1); } // even if no doughnut slice is hidden, sometimes percentageTotalShown is slightly less then 100 // we therefore use 99.9 to decide if slices are hidden (we only show with 0.1 precision) if (percentageTotalShown > 99.9) { // All items shown return label + ": " + itemPercentage + "%"; } else { // set percentageTotalShown again without rounding to account // for cases where the total shown percentage would be <0.1% of all percentageTotalShown = tooltipLabel.chart._metasets[0].total; return ( label + ":
• " + itemPercentage + "% of all queries
• " + ((tooltipLabel.parsed * 100) / percentageTotalShown).toFixed(1) + "% of shown items" ); } } // chartjs plugin used by the custom doughnut legend const getOrCreateLegendList = (chart, id) => { const legendContainer = document.getElementById(id); let listContainer = legendContainer.querySelector("ul"); if (!listContainer) { listContainer = document.createElement("ul"); listContainer.style.display = "flex"; listContainer.style.flexDirection = "column"; listContainer.style.margin = 0; listContainer.style.padding = 0; legendContainer.append(listContainer); } return listContainer; }; const htmlLegendPlugin = { id: "htmlLegend", afterUpdate(chart, args, options) { const ul = getOrCreateLegendList(chart, options.containerID); // Remove old legend items while (ul.firstChild) { ul.firstChild.remove(); } // Reuse the built-in legendItems generator const items = chart.options.plugins.legend.labels.generateLabels(chart); items.forEach(item => { const li = document.createElement("li"); li.style.alignItems = "center"; li.style.cursor = "pointer"; li.style.display = "flex"; li.style.flexDirection = "row"; // Color checkbox (toggle visibility) const boxSpan = document.createElement("span"); boxSpan.title = "Toggle visibility"; boxSpan.style.color = item.fillStyle; boxSpan.style.display = "inline-block"; boxSpan.style.margin = "0 10px"; boxSpan.innerHTML = item.hidden ? '' : ''; boxSpan.addEventListener("click", () => { const { type } = chart.config; if (type === "pie" || type === "doughnut") { // Pie and doughnut charts only have a single dataset and visibility is per item chart.toggleDataVisibility(item.index); } else { chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); } chart.update(); }); // Text (link to query log page) const textLink = document.createElement("p"); textLink.title = "List " + item.text + " queries"; textLink.style.color = item.fontColor; textLink.style.margin = 0; textLink.style.padding = 0; textLink.style.textDecoration = item.hidden ? "line-through" : ""; textLink.className = "legend-label-text"; textLink.append(item.text); textLink.addEventListener("click", () => { if (chart.canvas.id === "queryTypePieChart") { window.location.href = "queries.lp?querytype=" + querytypeids[item.index]; } else if (chart.canvas.id === "forwardDestinationPieChart") { window.location.href = "queries.lp?forwarddest=" + encodeURIComponent(item.text); } }); li.append(boxSpan, textLink); ul.append(li); }); }, }; $(function () { // Pull in data via AJAX updateSummaryData(); var gridColor = utils.getCSSval("graphs-grid", "background-color"); var ticksColor = utils.getCSSval("graphs-ticks", "color"); var ctx = document.getElementById("queryOverTimeChart").getContext("2d"); timeLineChart = new Chart(ctx, { type: utils.getGraphType(), data: { labels: [], datasets: [{ data: [], parsing: false }], }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: "nearest", axis: "x", }, plugins: { legend: { display: false, }, tooltip: { enabled: true, intersect: false, yAlign: "bottom", itemSort: function (a, b) { return b.datasetIndex - a.datasetIndex; }, callbacks: { title: function (tooltipTitle) { var label = tooltipTitle[0].label; var time = label.match(/(\d?\d):?(\d?\d?)/); var h = parseInt(time[1], 10); var m = parseInt(time[2], 10) || 0; var from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00"; var to = utils.padNumber(h) + ":" + utils.padNumber(m + 4) + ":59"; return "Queries from " + from + " to " + to; }, label: function (tooltipLabel) { var label = tooltipLabel.dataset.label; // Add percentage only for blocked queries if (tooltipLabel.datasetIndex === 0) { var percentage = 0; var permitted = parseInt(tooltipLabel.parsed._stacks.y[1], 10); var blocked = parseInt(tooltipLabel.parsed._stacks.y[0], 10); if (permitted + blocked > 0) { percentage = (100 * blocked) / (permitted + blocked); } label += ": " + tooltipLabel.parsed.y + " (" + percentage.toFixed(1) + "%)"; } else { label += ": " + tooltipLabel.parsed.y; } return label; }, }, }, }, scales: { xAxes: { type: "time", stacked: true, offset: false, time: { unit: "hour", displayFormats: { hour: "HH:mm", }, tooltipFormat: "HH:mm", }, grid: { color: gridColor, offset: false, drawBorder: false, }, ticks: { color: ticksColor, }, }, yAxes: { stacked: true, beginAtZero: true, ticks: { color: ticksColor, precision: 0, }, grid: { color: gridColor, drawBorder: false, }, }, }, elements: { line: { borderWidth: 0, spanGaps: false, fill: true, }, point: { radius: 0, hoverRadius: 5, hitRadius: 5, }, }, }, }); // Pull in data via AJAX updateQueriesOverTime(); // Create / load "Top Clients over Time" only if authorized var clientsChartEl = document.getElementById("clientsChart"); if (clientsChartEl) { ctx = clientsChartEl.getContext("2d"); clientsChart = new Chart(ctx, { type: utils.getGraphType(), data: { labels: [], datasets: [{ data: [], parsing: false }], }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: "nearest", axis: "x", }, plugins: { legend: { display: false, }, tooltip: { // Disable the on-canvas tooltip enabled: false, intersect: false, external: customTooltips, yAlign: "top", itemSort: function (a, b) { return b.raw - a.raw; }, callbacks: { title: function (tooltipTitle) { var label = tooltipTitle[0].label; var time = label.match(/(\d?\d):?(\d?\d?)/); var h = parseInt(time[1], 10); var m = parseInt(time[2], 10) || 0; var from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00"; var to = utils.padNumber(h) + ":" + utils.padNumber(m + 4) + ":59"; return "Client activity from " + from + " to " + to; }, }, }, }, scales: { xAxes: { type: "time", stacked: true, offset: false, time: { unit: "hour", displayFormats: { hour: "HH:mm", }, tooltipFormat: "HH:mm", }, grid: { color: gridColor, offset: false, drawBorder: false, }, ticks: { color: ticksColor, }, }, yAxes: { beginAtZero: true, ticks: { color: ticksColor, precision: 0, }, stacked: true, grid: { color: gridColor, drawBorder: false, }, }, }, elements: { line: { borderWidth: 0, spanGaps: false, fill: true, point: { radius: 0, hoverRadius: 5, hitRadius: 5, }, }, }, hover: { animationDuration: 0, }, }, }); // Pull in data via AJAX updateClientsOverTime(); } updateTopLists(); $("#queryOverTimeChart").on("click", function (evt) { var activePoints = timeLineChart.getElementsAtEventForMode( evt, "nearest", { intersect: true }, false ); if (activePoints.length > 0) { //get the internal index var clickedElementindex = activePoints[0].index; //get specific label by index var label = timeLineChart.data.labels[clickedElementindex]; //get value by index var from = label / 1000 - 300; var until = label / 1000 + 300; window.location.href = "queries.lp?from=" + from + "&until=" + until; } return false; }); $("#clientsChart").on("click", function (evt) { var activePoints = clientsChart.getElementsAtEventForMode( evt, "nearest", { intersect: true }, false ); if (activePoints.length > 0) { //get the internal index var clickedElementindex = activePoints[0].index; //get specific label by index var label = clientsChart.data.labels[clickedElementindex]; //get value by index var from = label / 1000 - 300; var until = label / 1000 + 300; window.location.href = "queries.lp?from=" + from + "&until=" + until; } return false; }); if (document.getElementById("queryTypePieChart")) { ctx = document.getElementById("queryTypePieChart").getContext("2d"); queryTypePieChart = new Chart(ctx, { type: "doughnut", data: { labels: [], datasets: [{ data: [], parsing: false }], }, plugins: [htmlLegendPlugin], options: { responsive: true, maintainAspectRatio: true, elements: { arc: { borderColor: $(".box").css("background-color"), }, }, plugins: { htmlLegend: { containerID: "query-types-legend", }, legend: { display: false, }, tooltip: { // Disable the on-canvas tooltip enabled: false, external: customTooltips, callbacks: { title: function () { return "Query type"; }, label: doughnutTooltip, }, }, }, animation: { duration: 750, }, }, }); // Pull in data via AJAX updateQueryTypesPie(); } if (document.getElementById("forwardDestinationPieChart")) { ctx = document.getElementById("forwardDestinationPieChart").getContext("2d"); forwardDestinationPieChart = new Chart(ctx, { type: "doughnut", data: { labels: [], datasets: [{ data: [], parsing: false }], }, plugins: [htmlLegendPlugin], options: { responsive: true, maintainAspectRatio: true, elements: { arc: { borderColor: $(".box").css("background-color"), }, }, plugins: { htmlLegend: { containerID: "forward-destinations-legend", }, legend: { display: false, }, tooltip: { // Disable the on-canvas tooltip enabled: false, external: customTooltips, callbacks: { title: function () { return "Upstream server"; }, label: doughnutTooltip, }, }, }, animation: { duration: 750, }, }, }); // Pull in data via AJAX updateForwardDestinationsPie(); } }); //destroy all chartjs customTooltips on window resize window.addEventListener("resize", function () { $(".chartjs-tooltip").remove(); });