enhance(client): heatmap for dashboard
This commit is contained in:
		
							parent
							
								
									eecd937e0a
								
							
						
					
					
						commit
						1aed1c587e
					
				
					 4 changed files with 258 additions and 0 deletions
				
			
		|  | @ -22,6 +22,7 @@ | ||||||
| 		"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", | 		"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", | ||||||
| 		"chart.js": "4.1.1", | 		"chart.js": "4.1.1", | ||||||
| 		"chartjs-adapter-date-fns": "3.0.0", | 		"chartjs-adapter-date-fns": "3.0.0", | ||||||
|  | 		"chartjs-chart-matrix": "^1.3.0", | ||||||
| 		"chartjs-plugin-gradient": "0.6.1", | 		"chartjs-plugin-gradient": "0.6.1", | ||||||
| 		"chartjs-plugin-zoom": "2.0.0", | 		"chartjs-plugin-zoom": "2.0.0", | ||||||
| 		"compare-versions": "5.0.1", | 		"compare-versions": "5.0.1", | ||||||
|  |  | ||||||
							
								
								
									
										233
									
								
								packages/client/src/pages/admin/overview.heatmap.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								packages/client/src/pages/admin/overview.heatmap.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,233 @@ | ||||||
|  | <template> | ||||||
|  | <div> | ||||||
|  | 	<MkLoading v-if="fetching"/> | ||||||
|  | 	<div v-show="!fetching" :class="$style.root" class="_panel"> | ||||||
|  | 		<canvas ref="chartEl"></canvas> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||||
|  | import { | ||||||
|  | 	Chart, | ||||||
|  | 	ArcElement, | ||||||
|  | 	LineElement, | ||||||
|  | 	BarElement, | ||||||
|  | 	PointElement, | ||||||
|  | 	BarController, | ||||||
|  | 	LineController, | ||||||
|  | 	CategoryScale, | ||||||
|  | 	LinearScale, | ||||||
|  | 	TimeScale, | ||||||
|  | 	Legend, | ||||||
|  | 	Title, | ||||||
|  | 	Tooltip, | ||||||
|  | 	SubTitle, | ||||||
|  | 	Filler, | ||||||
|  | } from 'chart.js'; | ||||||
|  | import { enUS } from 'date-fns/locale'; | ||||||
|  | import tinycolor from 'tinycolor2'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import 'chartjs-adapter-date-fns'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
|  | import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||||
|  | import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; | ||||||
|  | import { chartVLine } from '@/scripts/chart-vline'; | ||||||
|  | 
 | ||||||
|  | Chart.register( | ||||||
|  | 	ArcElement, | ||||||
|  | 	LineElement, | ||||||
|  | 	BarElement, | ||||||
|  | 	PointElement, | ||||||
|  | 	BarController, | ||||||
|  | 	LineController, | ||||||
|  | 	CategoryScale, | ||||||
|  | 	LinearScale, | ||||||
|  | 	TimeScale, | ||||||
|  | 	Legend, | ||||||
|  | 	Title, | ||||||
|  | 	Tooltip, | ||||||
|  | 	SubTitle, | ||||||
|  | 	Filler, | ||||||
|  | 	MatrixController, MatrixElement, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const alpha = (hex, a) => { | ||||||
|  | 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||||
|  | 	const r = parseInt(result[1], 16); | ||||||
|  | 	const g = parseInt(result[2], 16); | ||||||
|  | 	const b = parseInt(result[3], 16); | ||||||
|  | 	return `rgba(${r}, ${g}, ${b}, ${a})`; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const chartEl = $ref<HTMLCanvasElement>(null); | ||||||
|  | const now = new Date(); | ||||||
|  | let chartInstance: Chart = null; | ||||||
|  | const chartLimit = 7 * 20; | ||||||
|  | let fetching = $ref(true); | ||||||
|  | 
 | ||||||
|  | const { handler: externalTooltipHandler } = useChartTooltip({ | ||||||
|  | 	position: 'middle', | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function renderChart() { | ||||||
|  | 	if (chartInstance) { | ||||||
|  | 		chartInstance.destroy(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const getDate = (ago: number) => { | ||||||
|  | 		const y = now.getFullYear(); | ||||||
|  | 		const m = now.getMonth(); | ||||||
|  | 		const d = now.getDate(); | ||||||
|  | 
 | ||||||
|  | 		return new Date(y, m, d - ago); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const format = (arr) => { | ||||||
|  | 		return arr.map((v, i) => { | ||||||
|  | 			const dt = getDate(i); | ||||||
|  | 			const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`; | ||||||
|  | 			return { | ||||||
|  | 				x: iso, | ||||||
|  | 				y: dt.getDay(), | ||||||
|  | 				d: iso, | ||||||
|  | 				v, | ||||||
|  | 			}; | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' }); | ||||||
|  | 
 | ||||||
|  | 	const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||||
|  | 
 | ||||||
|  | 	// フォントカラー | ||||||
|  | 	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||||
|  | 
 | ||||||
|  | 	const color = '#3498db'; | ||||||
|  | 
 | ||||||
|  | 	const max = Math.max(...raw.readWrite); | ||||||
|  | 
 | ||||||
|  | 	const marginEachCell = 4; | ||||||
|  | 
 | ||||||
|  | 	chartInstance = new Chart(chartEl, { | ||||||
|  | 		type: 'matrix', | ||||||
|  | 		data: { | ||||||
|  | 			datasets: [{ | ||||||
|  | 				label: 'Read & Write', | ||||||
|  | 				data: format(raw.readWrite), | ||||||
|  | 				pointRadius: 0, | ||||||
|  | 				borderWidth: 0, | ||||||
|  | 				borderJoinStyle: 'round', | ||||||
|  | 				borderRadius: 4, | ||||||
|  | 				backgroundColor(c) { | ||||||
|  | 					const value = c.dataset.data[c.dataIndex].v; | ||||||
|  | 					const a = value / max; | ||||||
|  | 					return alpha(color, a); | ||||||
|  | 				}, | ||||||
|  | 				fill: true, | ||||||
|  | 				width(c) { | ||||||
|  | 					const a = c.chart.chartArea ?? {}; | ||||||
|  | 					// 20週間 | ||||||
|  | 					return (a.right - a.left) / 20 - marginEachCell; | ||||||
|  | 				}, | ||||||
|  | 				height(c) { | ||||||
|  | 					const a = c.chart.chartArea ?? {}; | ||||||
|  | 					// 7日 | ||||||
|  | 					return (a.bottom - a.top) / 7 - marginEachCell; | ||||||
|  | 				}, | ||||||
|  | 			}], | ||||||
|  | 		}, | ||||||
|  | 		options: { | ||||||
|  | 			aspectRatio: 2.8, | ||||||
|  | 			layout: { | ||||||
|  | 				padding: { | ||||||
|  | 					left: 8, | ||||||
|  | 					right: 0, | ||||||
|  | 					top: 0, | ||||||
|  | 					bottom: 0, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			scales: { | ||||||
|  | 				x: { | ||||||
|  | 					type: 'time', | ||||||
|  | 					offset: true, | ||||||
|  | 					position: 'bottom', | ||||||
|  | 					time: { | ||||||
|  | 						unit: 'week', | ||||||
|  | 						round: 'week', | ||||||
|  | 						isoWeekday: 0, | ||||||
|  | 						displayFormats: { | ||||||
|  | 							week: 'MMM dd', | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					grid: { | ||||||
|  | 						display: false, | ||||||
|  | 						color: gridColor, | ||||||
|  | 						borderColor: 'rgb(0, 0, 0, 0)', | ||||||
|  | 					}, | ||||||
|  | 					ticks: { | ||||||
|  | 						display: true, | ||||||
|  | 						maxRotation: 0, | ||||||
|  | 						autoSkipPadding: 8, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				y: { | ||||||
|  | 					offset: true, | ||||||
|  | 					reverse: true, | ||||||
|  | 					position: 'right', | ||||||
|  | 					grid: { | ||||||
|  | 						display: false, | ||||||
|  | 						color: gridColor, | ||||||
|  | 						borderColor: 'rgb(0, 0, 0, 0)', | ||||||
|  | 					}, | ||||||
|  | 					ticks: { | ||||||
|  | 						maxRotation: 0, | ||||||
|  | 						autoSkip: true, | ||||||
|  | 						padding: 1, | ||||||
|  | 						font: { | ||||||
|  | 							size: 9, | ||||||
|  | 						}, | ||||||
|  | 						callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value], | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			animation: false, | ||||||
|  | 			plugins: { | ||||||
|  | 				legend: { | ||||||
|  | 					display: false, | ||||||
|  | 				}, | ||||||
|  | 				tooltip: { | ||||||
|  | 					enabled: false, | ||||||
|  | 					callbacks: { | ||||||
|  | 						title() { | ||||||
|  | 							return ''; | ||||||
|  | 						}, | ||||||
|  | 						label(context) { | ||||||
|  | 							const v = context.dataset.data[context.dataIndex]; | ||||||
|  | 							return ['d: ' + v.d, 'v: ' + v.v.toFixed(2)]; | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					//mode: 'index', | ||||||
|  | 					animation: { | ||||||
|  | 						duration: 0, | ||||||
|  | 					}, | ||||||
|  | 					external: externalTooltipHandler, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	fetching = false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(async () => { | ||||||
|  | 	renderChart(); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" module> | ||||||
|  | .root { | ||||||
|  | 	padding: 20px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -5,34 +5,47 @@ | ||||||
| 			<template #header>Stats</template> | 			<template #header>Stats</template> | ||||||
| 			<XStats/> | 			<XStats/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Active users</template> | 			<template #header>Active users</template> | ||||||
| 			<XActiveUsers/> | 			<XActiveUsers/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
|  | 		<MkFolder class="item"> | ||||||
|  | 			<template #header>Heatmap</template> | ||||||
|  | 			<XHeatmap/> | ||||||
|  | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Moderators</template> | 			<template #header>Moderators</template> | ||||||
| 			<XModerators/> | 			<XModerators/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Federation</template> | 			<template #header>Federation</template> | ||||||
| 			<XFederation/> | 			<XFederation/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 		 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Instances</template> | 			<template #header>Instances</template> | ||||||
| 			<XInstances/> | 			<XInstances/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Ap requests</template> | 			<template #header>Ap requests</template> | ||||||
| 			<XApRequests/> | 			<XApRequests/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>New users</template> | 			<template #header>New users</template> | ||||||
| 			<XUsers/> | 			<XUsers/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Deliver queue</template> | 			<template #header>Deliver queue</template> | ||||||
| 			<XQueue domain="deliver"/> | 			<XQueue domain="deliver"/> | ||||||
| 		</MkFolder> | 		</MkFolder> | ||||||
|  | 
 | ||||||
| 		<MkFolder class="item"> | 		<MkFolder class="item"> | ||||||
| 			<template #header>Inbox queue</template> | 			<template #header>Inbox queue</template> | ||||||
| 			<XQueue domain="inbox"/> | 			<XQueue domain="inbox"/> | ||||||
|  | @ -51,6 +64,7 @@ import XUsers from './overview.users.vue'; | ||||||
| import XActiveUsers from './overview.active-users.vue'; | import XActiveUsers from './overview.active-users.vue'; | ||||||
| import XStats from './overview.stats.vue'; | import XStats from './overview.stats.vue'; | ||||||
| import XModerators from './overview.moderators.vue'; | import XModerators from './overview.moderators.vue'; | ||||||
|  | import XHeatmap from './overview.heatmap.vue'; | ||||||
| import MkTagCloud from '@/components/MkTagCloud.vue'; | import MkTagCloud from '@/components/MkTagCloud.vue'; | ||||||
| import { version, url } from '@/config'; | import { version, url } from '@/config'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
										
									
									
									
								
							|  | @ -4956,6 +4956,15 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
|  | "chartjs-chart-matrix@npm:^1.3.0": | ||||||
|  |   version: 1.3.0 | ||||||
|  |   resolution: "chartjs-chart-matrix@npm:1.3.0" | ||||||
|  |   peerDependencies: | ||||||
|  |     chart.js: ">=3.0.0" | ||||||
|  |   checksum: d29a08f3ffd888a1b6c45be2cbeb8987c145a74b07a713c84001860669b200931517746c475537dd0893c57a739115fa96a68d3a113013aff28f3bee4494d5cc | ||||||
|  |   languageName: node | ||||||
|  |   linkType: hard | ||||||
|  | 
 | ||||||
| "chartjs-plugin-gradient@npm:0.6.1": | "chartjs-plugin-gradient@npm:0.6.1": | ||||||
|   version: 0.6.1 |   version: 0.6.1 | ||||||
|   resolution: "chartjs-plugin-gradient@npm:0.6.1" |   resolution: "chartjs-plugin-gradient@npm:0.6.1" | ||||||
|  | @ -5165,6 +5174,7 @@ __metadata: | ||||||
|     browser-image-resizer: "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3" |     browser-image-resizer: "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3" | ||||||
|     chart.js: 4.1.1 |     chart.js: 4.1.1 | ||||||
|     chartjs-adapter-date-fns: 3.0.0 |     chartjs-adapter-date-fns: 3.0.0 | ||||||
|  |     chartjs-chart-matrix: ^1.3.0 | ||||||
|     chartjs-plugin-gradient: 0.6.1 |     chartjs-plugin-gradient: 0.6.1 | ||||||
|     chartjs-plugin-zoom: 2.0.0 |     chartjs-plugin-zoom: 2.0.0 | ||||||
|     compare-versions: 5.0.1 |     compare-versions: 5.0.1 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue