stats.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. // create views chart label
  2. function createViewsChartLabel(ctx) {
  3. const period = ctx.dataset.period;
  4. let labels = [];
  5. if (period === "day") {
  6. const nowHour = new Date().getHours();
  7. for (let i = 23; i >= 0; --i) {
  8. let h = nowHour - i;
  9. if (h < 0) h = 24 + h;
  10. labels.push(`${Math.floor(h)}:00`);
  11. }
  12. }
  13. if (period === "week") {
  14. const nowDay = new Date().getDate();
  15. for (let i = 6; i >= 0; --i) {
  16. const date = new Date(new Date().setDate(nowDay - i));
  17. labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
  18. }
  19. }
  20. if (period === "month") {
  21. const nowDay = new Date().getDate();
  22. for (let i = 29; i >= 0; --i) {
  23. const date = new Date(new Date().setDate(nowDay - i));
  24. labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
  25. }
  26. }
  27. if (period === "year") {
  28. const nowMonth = new Date().getMonth();
  29. for (let i = 11; i >= 0; --i) {
  30. const date = new Date(new Date().setMonth(nowMonth - i));
  31. labels.push(`${date.toLocaleString("default",{month:"short"})} ${date.toLocaleString("default",{year:"numeric"})}`);
  32. }
  33. }
  34. return labels;
  35. }
  36. // change stats period for showing charts and data
  37. function changeStatsPeriod(event) {
  38. const period = event.target.dataset.period;
  39. if (!period) return;
  40. const canvases = document.querySelector("#stats").querySelectorAll("[data-period]");
  41. const buttons = document.querySelector("#stats").querySelectorAll(".nav");
  42. if (!buttons || !canvases) return;
  43. buttons.forEach(b => b.disabled = false);
  44. event.target.disabled = true;
  45. canvases.forEach(canvas => {
  46. if (canvas.dataset.period === period) {
  47. canvas.classList.remove("hidden");
  48. } else {
  49. canvas.classList.add("hidden");
  50. }
  51. });
  52. feedMapData(period);
  53. }
  54. // beautify browser lables
  55. function beautifyBrowserName(name) {
  56. if (name === "firefox") return "Firefox";
  57. if (name === "chrome") return "Chrome";
  58. if (name === "edge") return "Edge";
  59. if (name === "opera") return "Opera";
  60. if (name === "safari") return "Safari";
  61. if (name === "other") return "Other";
  62. if (name === "ie") return "IE";
  63. return name;
  64. }
  65. // create views chart
  66. function createViewsChart() {
  67. const canvases = document.querySelectorAll("canvas.visits");
  68. if (!canvases || !canvases.length) return;
  69. canvases.forEach(ctx => {
  70. const data = JSON.parse(ctx.dataset.data);
  71. const period = ctx.dataset.period;
  72. const labels = createViewsChartLabel(ctx);
  73. const maxTicksLimitX = period === "year" ? 6 : period === "month" ? 15 : 12;
  74. const gradient = ctx.getContext("2d").createLinearGradient(0, 0, 0, 300);
  75. gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
  76. gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
  77. new Chart(ctx, {
  78. type: "line",
  79. data: {
  80. labels: labels,
  81. datasets: [{
  82. label: "Views",
  83. data,
  84. tension: 0.3,
  85. elements: {
  86. point: {
  87. pointRadius: 0,
  88. pointHoverRadius: 4
  89. }
  90. },
  91. fill: {
  92. target: "start",
  93. },
  94. backgroundColor: gradient,
  95. borderColor: "rgb(179, 157, 219)",
  96. borderWidth: 1,
  97. }]
  98. },
  99. options: {
  100. plugins: {
  101. legend: {
  102. display: false,
  103. },
  104. tooltip: {
  105. backgroundColor: "rgba(255, 255, 255, 0.95)",
  106. titleColor: "#333",
  107. titleFont: { weight: "normal", size: 15 },
  108. bodyFont: { weight: "normal", size: 16 },
  109. bodyColor: "rgb(179, 157, 219)",
  110. padding: 12,
  111. cornerRadius: 2,
  112. borderColor: "rgba(0, 0, 0, 0.1)",
  113. borderWidth: 1,
  114. displayColors: false,
  115. }
  116. },
  117. responsive: true,
  118. interaction: {
  119. intersect: false,
  120. usePointStyle: true,
  121. mode: "index",
  122. },
  123. scales: {
  124. y: {
  125. grace: "10%",
  126. beginAtZero: true,
  127. ticks: {
  128. maxTicksLimit: 5
  129. }
  130. },
  131. x: {
  132. ticks: {
  133. maxTicksLimit: maxTicksLimitX,
  134. }
  135. }
  136. }
  137. }
  138. });
  139. // reset the display: block style that chart.js applies automatically
  140. ctx.style.display = "";
  141. });
  142. }
  143. // create browsers chart
  144. function createBrowsersChart() {
  145. const canvases = document.querySelectorAll("canvas.browsers");
  146. if (!canvases || !canvases.length) return;
  147. canvases.forEach(ctx => {
  148. const data = JSON.parse(ctx.dataset.data);
  149. const period = ctx.dataset.period;
  150. const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  151. const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  152. gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
  153. gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
  154. gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");
  155. gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
  156. new Chart(ctx, {
  157. type: "bar",
  158. data: {
  159. labels: data.map(d => beautifyBrowserName(d.name)),
  160. datasets: [{
  161. label: "Views",
  162. data: data.map(d => d.value),
  163. backgroundColor: gradient,
  164. borderColor: "rgba(179, 157, 219, 1)",
  165. borderWidth: 1,
  166. hoverBackgroundColor: gradientHover,
  167. hoverBorderWidth: 2
  168. }]
  169. },
  170. options: {
  171. indexAxis: "y",
  172. plugins: {
  173. legend: {
  174. display: false,
  175. },
  176. tooltip: {
  177. backgroundColor: "rgba(255, 255, 255, 0.95)",
  178. titleColor: "#333",
  179. titleFont: { weight: "normal", size: 15 },
  180. bodyFont: { weight: "normal", size: 16 },
  181. bodyColor: "rgb(179, 157, 219)",
  182. padding: 12,
  183. cornerRadius: 2,
  184. borderColor: "rgba(0, 0, 0, 0.1)",
  185. borderWidth: 1,
  186. displayColors: false,
  187. }
  188. },
  189. responsive: true,
  190. interaction: {
  191. intersect: false,
  192. mode: "index",
  193. axis: "y"
  194. },
  195. scales: {
  196. x: {
  197. grace: "5%",
  198. beginAtZero: true,
  199. ticks: {
  200. maxTicksLimit: 6,
  201. }
  202. }
  203. }
  204. }
  205. });
  206. // reset the display: block style that chart.js applies automatically
  207. ctx.style.display = "";
  208. });
  209. }
  210. // create referrers chart
  211. function createReferrersChart() {
  212. const canvases = document.querySelectorAll("canvas.referrers");
  213. if (!canvases || !canvases.length) return;
  214. canvases.forEach(ctx => {
  215. const data = JSON.parse(ctx.dataset.data);
  216. const period = ctx.dataset.period;
  217. let max = Array.from(data).sort((a, b) => a.value > b.value ? -1 : 1)[0];
  218. let tooltipEnabled = true;
  219. let hoverBackgroundColor = "rgba(179, 157, 219, 1)";
  220. let hoverBorderWidth = 2;
  221. let borderColor = "rgba(179, 157, 219, 1)";
  222. if (data.length === 0) {
  223. data.push({ name: "No views.", value: 1 });
  224. max = { value: 1000 };
  225. tooltipEnabled = false;
  226. hoverBackgroundColor = "rgba(179, 157, 219, 0.1)";
  227. hoverBorderWidth = 1;
  228. borderColor = "rgba(179, 157, 219, 0.2)";
  229. }
  230. new Chart(ctx, {
  231. type: "doughnut",
  232. data: {
  233. labels: data.map(d => d.name.replace(/\[dot\]/g, ".")),
  234. datasets: [{
  235. label: "Views",
  236. data: data.map(d => d.value),
  237. backgroundColor: data.map(d => `rgba(179, 157, 219, ${Math.max((d.value / max.value) - 0.2, 0.1).toFixed(2)})`),
  238. borderWidth: 1,
  239. borderColor,
  240. hoverBackgroundColor,
  241. hoverBorderWidth,
  242. }]
  243. },
  244. options: {
  245. plugins: {
  246. legend: {
  247. position: "left",
  248. labels: {
  249. boxWidth: 25,
  250. font: { size: 11 }
  251. }
  252. },
  253. tooltip: {
  254. enabled: tooltipEnabled,
  255. backgroundColor: "rgba(255, 255, 255, 0.95)",
  256. titleColor: "#333",
  257. titleFont: { weight: "normal", size: 15 },
  258. bodyFont: { weight: "normal", size: 16 },
  259. bodyColor: "rgb(179, 157, 219)",
  260. padding: 12,
  261. cornerRadius: 2,
  262. borderColor: "rgba(0, 0, 0, 0.1)",
  263. borderWidth: 1,
  264. displayColors: false,
  265. }
  266. },
  267. responsive: false,
  268. }
  269. });
  270. // reset the display: block style that chart.js applies automatically
  271. ctx.style.display = "";
  272. });
  273. }
  274. // beautify browser lables
  275. function beautifyOsName(name) {
  276. if (name === "android") return "Android";
  277. if (name === "ios") return "iOS";
  278. if (name === "linux") return "Linux";
  279. if (name === "macos") return "macOS";
  280. if (name === "windows") return "Windows";
  281. if (name === "other") return "Other";
  282. return name;
  283. }
  284. // create operating systems chart
  285. function createOsChart() {
  286. const canvases = document.querySelectorAll("canvas.os");
  287. if (!canvases || !canvases.length) return;
  288. canvases.forEach(ctx => {
  289. const data = JSON.parse(ctx.dataset.data);
  290. const period = ctx.dataset.period;
  291. const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  292. const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  293. gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
  294. gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
  295. gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");
  296. gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
  297. new Chart(ctx, {
  298. type: "bar",
  299. data: {
  300. labels: data.map(d => beautifyOsName(d.name)),
  301. datasets: [{
  302. label: "Views",
  303. data: data.map(d => d.value),
  304. backgroundColor: gradient,
  305. borderColor: "rgba(179, 157, 219, 1)",
  306. borderWidth: 1,
  307. hoverBackgroundColor: gradientHover,
  308. hoverBorderWidth: 2
  309. }]
  310. },
  311. options: {
  312. indexAxis: "y",
  313. plugins: {
  314. legend: {
  315. display: false,
  316. },
  317. tooltip: {
  318. backgroundColor: "rgba(255, 255, 255, 0.95)",
  319. titleColor: "#333",
  320. titleFont: { weight: "normal", size: 15 },
  321. bodyFont: { weight: "normal", size: 16 },
  322. bodyColor: "rgb(179, 157, 219)",
  323. padding: 12,
  324. cornerRadius: 2,
  325. borderColor: "rgba(0, 0, 0, 0.1)",
  326. borderWidth: 1,
  327. displayColors: false,
  328. }
  329. },
  330. responsive: true,
  331. interaction: {
  332. intersect: false,
  333. mode: "index",
  334. axis: "y"
  335. },
  336. scales: {
  337. x: {
  338. grace:"5%",
  339. beginAtZero: true,
  340. ticks: {
  341. maxTicksLimit: 6,
  342. }
  343. }
  344. }
  345. }
  346. });
  347. // reset the display: block style that chart.js applies automatically
  348. ctx.style.display = "";
  349. });
  350. }
  351. // add data to the map
  352. function feedMapData(period) {
  353. const map = document.querySelector("svg.map");
  354. const paths = map.querySelectorAll("path");
  355. if (!map || !paths || !paths.length) return;
  356. let data = JSON.parse(map.dataset[period || "day"]);
  357. if (!data) return;
  358. let max = data.sort((a, b) => a.value > b.value ? -1 : 1)[0];
  359. if (!max) max = { value: 1 }
  360. data = data.reduce((a, c) => ({ ...a, [c.name]: c.value }), {});
  361. for (let i = 0; i < paths.length; ++i) {
  362. const id = paths[i].dataset.id;
  363. const views = data[id] || 0;
  364. paths[i].dataset.views = views;
  365. const colorLevel = Math.ceil((views / max.value) * 6);
  366. const classList = paths[i].classList;
  367. for (let j = 1; j < 7; j++) {
  368. paths[i].classList.remove(`color-${j}`);
  369. }
  370. paths[i].classList.add(`color-${colorLevel}`)
  371. paths[i].dataset.views = views;
  372. }
  373. }
  374. // handle map tooltip hover
  375. function mapTooltipHoverOver() {
  376. const tooltip = document.querySelector("#map-tooltip");
  377. if (!tooltip) return;
  378. if (!event.target.dataset.id) return mapTooltipHoverOut();
  379. if (!tooltip.classList.contains("active")) {
  380. tooltip.classList.add("visible");
  381. }
  382. tooltip.dataset.tooltip = `${event.target.ariaLabel}: ${event.target.dataset.views || 0}`;
  383. const rect = event.target.getBoundingClientRect();
  384. tooltip.style.top = rect.top + (rect.height / 2) + "px";
  385. tooltip.style.left = rect.left + (rect.width / 2) + "px";
  386. event.target.classList.add("active");
  387. }
  388. function mapTooltipHoverOut() {
  389. const tooltip = document.querySelector("#map-tooltip");
  390. const map = document.querySelector("svg.map");
  391. const paths = map.querySelectorAll("path");
  392. if (!tooltip || !map) return;
  393. tooltip.classList.remove("visible");
  394. for (let i = 0; i < paths.length; ++i) {
  395. paths[i].classList.remove("active");
  396. }
  397. }
  398. // create stats charts
  399. function createCharts() {
  400. if (Chart === undefined) {
  401. setTimeout(function() { createCharts() }, 100);
  402. return;
  403. }
  404. createViewsChart();
  405. createBrowsersChart();
  406. createReferrersChart();
  407. createOsChart();
  408. feedMapData();
  409. }