nodeDataAnalysis.blade.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. @extends('admin.layouts')
  2. @section('css')
  3. <link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
  4. <link href="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.css" rel="stylesheet">
  5. @endsection
  6. @section('content')
  7. <div class="page-content container-fluid w-xl-p75 w-xxl-p100">
  8. <div class="card card-shadow">
  9. <div class="card-block p-30">
  10. <form class="form-row">
  11. <div class="form-group col-xxl-2 col-lg-3 col-md-3 col-sm-4">
  12. <select class="form-control show-tick" id="hour_date" name="hour_date" data-plugin="selectpicker" data-style="btn-outline btn-primary"
  13. title="{{ trans('admin.report.select_hourly_date') }}" onchange="cleanSubmit(this.form);this.form.submit()">
  14. @foreach ($hour_dates as $date)
  15. <option value="{{ $date }}">{{ $date }}</option>
  16. @endforeach
  17. </select>
  18. </div>
  19. <div class="form-group col-xxl-2 col-lg-3 col-md-3 col-sm-4">
  20. <select class="form-control show-tick" id="nodes" name="nodes[]" data-plugin="selectpicker" data-style="btn-outline btn-primary"
  21. title="{{ trans('admin.logs.user_traffic.choose_node') }}" multiple>
  22. @foreach ($nodes as $id => $name)
  23. <option value="{{ $id }}">{{ $name }}</option>
  24. @endforeach
  25. </select>
  26. </div>
  27. <div class="form-group col-lg-6 col-sm-12">
  28. <div class="input-group input-daterange" data-plugin="datepicker">
  29. <div class="input-group-prepend">
  30. <span class="input-group-text"><i class="icon wb-calendar" aria-hidden="true"></i></span>
  31. </div>
  32. <input class="form-control" name="start" type="text" value="{{ Request::query('start') }}" autocomplete="off" />
  33. <div class="input-group-prepend">
  34. <span class="input-group-text">{{ trans('common.to') }}</span>
  35. </div>
  36. <input class="form-control" name="end" type="text" value="{{ Request::query('end') }}" autocomplete="off" />
  37. </div>
  38. </div>
  39. <div class="form-group col-xxl-1 col-lg-3 col-md-3 col-4 btn-group">
  40. <button class="btn btn-primary" type="submit">{{ trans('common.search') }}</button>
  41. <button class="btn btn-danger" type="button" onclick="resetSearchForm()">{{ trans('common.reset') }}</button>
  42. </div>
  43. </form>
  44. </div>
  45. </div>
  46. @isset($data)
  47. <div class="row mx-0">
  48. <div class="col-md-12 col-xxl-7 card card-shadow">
  49. <div class="card-block p-md-30">
  50. <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.hourly_traffic') }}</div>
  51. <canvas id="hourlyBar"></canvas>
  52. </div>
  53. </div>
  54. <div class="col-md-12 col-xxl-5 card card-shadow">
  55. <div class="card-block p-md-30">
  56. <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.daily_distribution') }}</div>
  57. <div class="d-flex justify-content-around">
  58. <canvas id="dailyPie"></canvas>
  59. </div>
  60. </div>
  61. </div>
  62. <div class="col-12 offset-xxl-2 col-xxl-8 card card-shadow">
  63. <div class="card-block p-md-30">
  64. <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.daily_traffic') }}</div>
  65. <canvas id="dailyBar"></canvas>
  66. </div>
  67. </div>
  68. </div>
  69. @endisset
  70. </div>
  71. @endsection
  72. @section('javascript')
  73. <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
  74. <script src="/assets/global/vendor/chart-js/chartjs-plugin-datalabels.min.js"></script>
  75. <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
  76. <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
  77. <script src="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js"></script>
  78. <script src="/assets/global/js/Plugin/bootstrap-datepicker.js"></script>
  79. <script type="text/javascript">
  80. const nodeData = @json($data);
  81. const nodeColorMap = generateNodeColorMap(nodeData.nodes);
  82. function resetSearchForm() {
  83. window.location.href = window.location.href.split('?')[0];
  84. }
  85. $('.input-daterange').datepicker({
  86. format: 'yyyy-mm-dd',
  87. startDate: nodeData.start_date,
  88. endDate: new Date(),
  89. });
  90. $('form').on('submit', function() {
  91. cleanSubmit(this);
  92. });
  93. function cleanSubmit(form) {
  94. $(form).find('input:not([type="submit"]), select').filter(function() {
  95. return this.value === "";
  96. }).prop('disabled', true);
  97. setTimeout(function() {
  98. $(form).find(':disabled').prop('disabled', false);
  99. }, 0);
  100. }
  101. function optimizeDatasets(datasets) {
  102. const dataByDate = datasets.reduce((acc, dataset) => {
  103. dataset.data.forEach(item => {
  104. acc[item.time] = acc[item.time] || [];
  105. acc[item.time].push({
  106. id: dataset.label,
  107. total: parseFloat(item.total)
  108. });
  109. });
  110. return acc;
  111. }, {});
  112. const allNodeIds = datasets.map(d => d.label);
  113. const optimizedData = Object.entries(dataByDate).map(([date, dayData]) => {
  114. const total = dayData.reduce((sum, item) => sum + item.total, 0);
  115. const filledData = allNodeIds.map(id => {
  116. const nodeData = dayData.find(item => item.id === id);
  117. return nodeData ? nodeData.total : 0;
  118. });
  119. return {
  120. time: date,
  121. data: filledData,
  122. total
  123. };
  124. });
  125. return datasets.map((dataset, index) => ({
  126. ...dataset,
  127. data: optimizedData.map(day => ({
  128. time: day.time,
  129. total: day.data[index]
  130. }))
  131. }));
  132. }
  133. function createBarChart(elementId, labels, datasets, labelTail, unit = 'MiB') {
  134. const optimizedDatasets = optimizeDatasets(datasets);
  135. new Chart(document.getElementById(elementId), {
  136. type: 'bar',
  137. data: {
  138. labels: optimizedDatasets[0]?.data.map(d => d.time),
  139. datasets: optimizedDatasets
  140. },
  141. plugins: [ChartDataLabels],
  142. options: {
  143. parsing: {
  144. xAxisKey: 'time',
  145. yAxisKey: 'total'
  146. },
  147. scales: {
  148. x: {
  149. stacked: true
  150. },
  151. y: {
  152. stacked: true
  153. }
  154. },
  155. responsive: true,
  156. plugins: {
  157. legend: {
  158. labels: {
  159. padding: 10,
  160. usePointStyle: true,
  161. pointStyle: 'circle',
  162. font: {
  163. size: 14
  164. }
  165. },
  166. },
  167. tooltip: label_callbacks(labelTail, unit),
  168. datalabels: {
  169. display: true,
  170. align: 'end',
  171. anchor: 'end',
  172. formatter: (value, context) => {
  173. if (context.datasetIndex === context.chart.data.datasets.length - 1) {
  174. let total = context.chart.data.datasets.reduce((sum, dataset) => sum + dataset.data[context.dataIndex].total, 0);
  175. return total.toFixed(2) + unit;
  176. }
  177. return null;
  178. },
  179. },
  180. },
  181. },
  182. });
  183. }
  184. function label_callbacks(tail, unit) {
  185. return {
  186. mode: 'index',
  187. intersect: false,
  188. callbacks: {
  189. title: context => `${context[0].label} ${tail}`,
  190. label: context => {
  191. const dataset = context.dataset;
  192. const value = dataset.data[context.dataIndex]?.total || context.parsed.y;
  193. return `${dataset.label || ''}: ${value.toFixed(2)} ${unit}`;
  194. }
  195. }
  196. };
  197. }
  198. function generateNodeColorMap(nodeNames) {
  199. return Object.fromEntries(Object.entries(nodeNames).map(([id, name]) => [id, getRandomColor(name)]));
  200. }
  201. function getRandomColor(name) {
  202. let hash = 0;
  203. for (let i = 0; i < name.length; i++) {
  204. hash = name.charCodeAt(i) + ((hash << 5) - hash);
  205. }
  206. const hue = (hash % 360 + Math.random() * 50) % 360;
  207. const saturation = 50 + (hash % 30);
  208. const lightness = 40 + (hash % 20);
  209. return `hsla(${hue}, ${saturation}%, ${lightness}%, 0.55)`;
  210. }
  211. function generateDatasets(flows) {
  212. const dataByNode = flows.reduce((acc, flow) => {
  213. acc[flow.id] = acc[flow.id] || [];
  214. acc[flow.id].push({
  215. time: flow.time,
  216. total: parseFloat(flow.total),
  217. name: flow.name
  218. });
  219. return acc;
  220. }, {});
  221. return Object.entries(dataByNode).map(([nodeId, data]) => ({
  222. label: data[0].name,
  223. backgroundColor: nodeColorMap[nodeId],
  224. borderColor: nodeColorMap[nodeId],
  225. data,
  226. fill: true,
  227. }));
  228. }
  229. function createDoughnutChart(elementId, labels, data, colors, date) {
  230. Chart.register({
  231. id: 'totalLabel',
  232. beforeDraw(chart) {
  233. const {
  234. ctx,
  235. chartArea,
  236. data
  237. } = chart;
  238. if (!chartArea || !data.datasets.length) return;
  239. const total = data.datasets[0].data.reduce((acc, val) => acc + val, 0);
  240. if (typeof total !== 'number') return;
  241. const {
  242. width,
  243. height,
  244. top,
  245. left
  246. } = chartArea;
  247. const text = `${date}\n${total.toFixed(2)} GiB`;
  248. ctx.save();
  249. ctx.font = 'bold 32px Roboto';
  250. ctx.fillStyle = 'black';
  251. ctx.textAlign = 'center';
  252. ctx.textBaseline = 'middle';
  253. const lineHeight = 40;
  254. text.split('\n').forEach((line, index) => {
  255. ctx.fillText(line, left + width / 2, top + height / 2 - lineHeight / 2 + index * lineHeight);
  256. });
  257. ctx.restore();
  258. },
  259. });
  260. new Chart(document.getElementById(elementId), {
  261. type: 'doughnut',
  262. data: {
  263. labels,
  264. datasets: [{
  265. data,
  266. backgroundColor: colors
  267. }]
  268. },
  269. options: {
  270. responsive: true,
  271. plugins: {
  272. legend: {
  273. display: false
  274. },
  275. tooltip: {
  276. callbacks: {
  277. label: tooltipItem => {
  278. const dataset = tooltipItem.dataset;
  279. const currentValue = dataset.data[tooltipItem.dataIndex];
  280. const label = tooltipItem.label || '';
  281. return `${label}: ${currentValue.toFixed(2)} G`;
  282. },
  283. },
  284. },
  285. datalabels: {
  286. color: '#fff',
  287. formatter: (value, context) => {
  288. const total = context.dataset.data.reduce((sum, val) => sum + val, 0);
  289. const percentage = (value / total * 100).toFixed(1);
  290. const label = context.chart.data.labels[context.dataIndex];
  291. return percentage > 1 ? `${label} ${percentage}%` : '';
  292. },
  293. anchor: "center",
  294. rotation: function(ctx) {
  295. const valuesBefore = ctx.dataset.data.slice(0, ctx.dataIndex).reduce((a, b) => a + b, 0);
  296. const sum = ctx.dataset.data.reduce((a, b) => a + b, 0);
  297. const rotation = ((valuesBefore + ctx.dataset.data[ctx.dataIndex] / 2) / sum * 360);
  298. return rotation < 180 ? rotation - 90 : rotation + 90;
  299. },
  300. font: {
  301. weight: 'bold',
  302. size: 16,
  303. family: 'Roboto'
  304. },
  305. },
  306. },
  307. },
  308. plugins: [ChartDataLabels, 'totalLabel'],
  309. });
  310. }
  311. function generatePieData(flows) {
  312. return {
  313. labels: flows.map(flow => flow.name),
  314. data: flows.map(flow => flow.total),
  315. colors: flows.map(flow => nodeColorMap[flow.id]),
  316. };
  317. }
  318. function initCharts() {
  319. createBarChart('hourlyBar', nodeData.hours, generateDatasets(nodeData.hourlyFlows), @json(trans_choice('common.hour', 2)), ' GiB');
  320. createBarChart('dailyBar', '', generateDatasets(nodeData.dailyFlows), '', ' GiB');
  321. const lastDate = nodeData.dailyFlows[nodeData.dailyFlows.length - 1].time;
  322. const lastDayData = nodeData.dailyFlows.filter(flow => flow.time === lastDate);
  323. const {
  324. labels,
  325. data,
  326. colors
  327. } = generatePieData(lastDayData);
  328. createDoughnutChart('dailyPie', labels, data, colors, lastDate);
  329. }
  330. $(document).ready(function() {
  331. $('#nodes').selectpicker('val', @json(Request::query('nodes')));
  332. $('#hour_date').selectpicker('val', @json(Request::query('hour_date')));
  333. $('.input-daterange').datepicker('update', @json(Request::query('start')), @json(Request::query('end')));
  334. initCharts();
  335. });
  336. </script>
  337. @endsection