enhance(client): user activity page
This commit is contained in:
		
							parent
							
								
									1df23a839a
								
							
						
					
					
						commit
						7a95339296
					
				
					 17 changed files with 564 additions and 339 deletions
				
			
		|  | @ -916,6 +916,7 @@ caption: "キャプション" | |||
| loggedInAsBot: "Botアカウントでログイン中" | ||||
| tools: "ツール" | ||||
| cannotLoad: "読み込めません" | ||||
| numberOfProfileView: "プロフィール表示回数" | ||||
| 
 | ||||
| _sensitiveMediaDetection: | ||||
|   description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" | ||||
|  |  | |||
|  | @ -14,26 +14,9 @@ | |||
|   As this is part of Chart.js's API it makes sense to disable the check here. | ||||
| */ | ||||
| import { onMounted, ref, watch, PropType, onUnmounted } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import zoomPlugin from 'chartjs-plugin-zoom'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
|  | @ -41,6 +24,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | |||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import date from '@/filters/date'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps({ | ||||
| 	src: { | ||||
|  | @ -82,25 +68,6 @@ const props = defineProps({ | |||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	zoomPlugin, | ||||
| 	gradient, | ||||
| ); | ||||
| 
 | ||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||
| const negate = arr => arr.map(x => -x); | ||||
| 
 | ||||
|  | @ -742,6 +709,33 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { | |||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| const fetchPerUserPvChart = async (): Promise<typeof chartData> => { | ||||
| 	const raw = await os.apiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span }); | ||||
| 	return { | ||||
| 		series: [{ | ||||
| 			name: 'Unique PV (user)', | ||||
| 			type: 'area', | ||||
| 			data: format(raw.upv.user), | ||||
| 			color: colors.purple, | ||||
| 		}, { | ||||
| 			name: 'PV (user)', | ||||
| 			type: 'area', | ||||
| 			data: format(raw.pv.user), | ||||
| 			color: colors.green, | ||||
| 		}, { | ||||
| 			name: 'Unique PV (visitor)', | ||||
| 			type: 'area', | ||||
| 			data: format(raw.upv.visitor), | ||||
| 			color: colors.yellow, | ||||
| 		}, { | ||||
| 			name: 'PV (visitor)', | ||||
| 			type: 'area', | ||||
| 			data: format(raw.pv.visitor), | ||||
| 			color: colors.blue, | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { | ||||
| 	const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); | ||||
| 	return { | ||||
|  | @ -814,6 +808,7 @@ const fetchAndRender = async () => { | |||
| 			case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); | ||||
| 
 | ||||
| 			case 'per-user-notes': return fetchPerUserNotesChart(); | ||||
| 			case 'per-user-pv': return fetchPerUserPvChart(); | ||||
| 			case 'per-user-following': return fetchPerUserFollowingChart(); | ||||
| 			case 'per-user-followers': return fetchPerUserFollowersChart(); | ||||
| 			case 'per-user-drive': return fetchPerUserDriveChart(); | ||||
|  |  | |||
|  | @ -9,23 +9,7 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as os from '@/os'; | ||||
|  | @ -35,24 +19,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | |||
| import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	MatrixController, MatrixElement, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	src: string; | ||||
|  |  | |||
|  | @ -77,24 +77,7 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { onMounted } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	DoughnutController, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import MkChart from '@/components/MkChart.vue'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
|  | @ -103,24 +86,9 @@ import { i18n } from '@/i18n'; | |||
| import MkHeatmap from '@/components/MkHeatmap.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	DoughnutController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const chartLimit = 500; | ||||
| let chartSpan = $ref<'hour' | 'day'>('hour'); | ||||
|  |  | |||
|  | @ -9,23 +9,7 @@ | |||
| 
 | ||||
| <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 { Chart } from 'chart.js'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as os from '@/os'; | ||||
|  | @ -35,24 +19,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | |||
| import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	MatrixController, MatrixElement, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const rootEl = $ref<HTMLDivElement>(null); | ||||
| const chartEl = $ref<HTMLCanvasElement>(null); | ||||
|  |  | |||
|  | @ -52,21 +52,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import MkwFederation from '../../widgets/federation.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
|  | @ -79,21 +65,9 @@ import number from '@/filters/number'; | |||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  |  | |||
|  | @ -9,23 +9,7 @@ | |||
| 
 | ||||
| <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 { Chart } from 'chart.js'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as os from '@/os'; | ||||
|  | @ -35,24 +19,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | |||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	gradient, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const chartEl = $ref<HTMLCanvasElement>(null); | ||||
| const now = new Date(); | ||||
|  |  | |||
|  | @ -16,23 +16,7 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
|  | @ -45,24 +29,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | |||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	gradient, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const chartLimit = 50; | ||||
| const chartEl = $ref<HTMLCanvasElement>(); | ||||
|  |  | |||
|  | @ -4,45 +4,13 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	DoughnutController, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import number from '@/filters/number'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	DoughnutController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	data: { name: string; value: number; color: string; onClick?: () => void }[]; | ||||
|  |  | |||
|  | @ -4,46 +4,16 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { watch, onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import number from '@/filters/number'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	type: string; | ||||
|  |  | |||
|  | @ -4,46 +4,16 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { watch, onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import number from '@/filters/number'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| ); | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	type: string; | ||||
|  |  | |||
							
								
								
									
										217
									
								
								packages/frontend/src/pages/user/activity.heatmap.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								packages/frontend/src/pages/user/activity.heatmap.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,217 @@ | |||
| <template> | ||||
| <div ref="rootEl"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div v-else> | ||||
| 		<canvas ref="chartEl"></canvas> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import * as os from '@/os'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	src: string; | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
| 
 | ||||
| const rootEl = $ref<HTMLDivElement>(null); | ||||
| const chartEl = $ref<HTMLCanvasElement>(null); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| let fetching = $ref(true); | ||||
| 
 | ||||
| const { handler: externalTooltipHandler } = useChartTooltip({ | ||||
| 	position: 'middle', | ||||
| }); | ||||
| 
 | ||||
| async function renderChart() { | ||||
| 	if (chartInstance) { | ||||
| 		chartInstance.destroy(); | ||||
| 	} | ||||
| 
 | ||||
| 	const wide = rootEl.offsetWidth > 700; | ||||
| 	const narrow = rootEl.offsetWidth < 400; | ||||
| 
 | ||||
| 	const weeks = wide ? 50 : narrow ? 10 : 25; | ||||
| 	const chartLimit = 7 * weeks; | ||||
| 
 | ||||
| 	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, | ||||
| 			}; | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	let values; | ||||
| 
 | ||||
| 	if (props.src === 'notes') { | ||||
| 		const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
| 		values = raw.inc; | ||||
| 	} | ||||
| 
 | ||||
| 	fetching = false; | ||||
| 
 | ||||
| 	await nextTick(); | ||||
| 
 | ||||
| 	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 = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; | ||||
| 
 | ||||
| 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする | ||||
| 	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; | ||||
| 
 | ||||
| 	const min = Math.max(0, Math.min(...values) - 1); | ||||
| 
 | ||||
| 	const marginEachCell = 4; | ||||
| 
 | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'matrix', | ||||
| 		data: { | ||||
| 			datasets: [{ | ||||
| 				label: '', | ||||
| 				data: format(values), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 3, | ||||
| 				backgroundColor(c) { | ||||
| 					const value = c.dataset.data[c.dataIndex].v; | ||||
| 					let a = (value - min) / max; | ||||
| 					if (value !== 0) { // 0でない限りは完全に不可視にはしない | ||||
| 						a = Math.max(a, 0.05); | ||||
| 					} | ||||
| 					return alpha(color, a); | ||||
| 				}, | ||||
| 				fill: true, | ||||
| 				width(c) { | ||||
| 					const a = c.chart.chartArea ?? {}; | ||||
| 					return (a.right - a.left) / weeks - marginEachCell; | ||||
| 				}, | ||||
| 				height(c) { | ||||
| 					const a = c.chart.chartArea ?? {}; | ||||
| 					return (a.bottom - a.top) / 7 - marginEachCell; | ||||
| 				}, | ||||
| 			}], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2, | ||||
| 			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(context) { | ||||
| 							const v = context[0].dataset.data[context[0].dataIndex]; | ||||
| 							return v.d; | ||||
| 						}, | ||||
| 						label(context) { | ||||
| 							const v = context.dataset.data[context.dataIndex]; | ||||
| 							return [v.v]; | ||||
| 						}, | ||||
| 					}, | ||||
| 					//mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch(() => props.src, () => { | ||||
| 	fetching = true; | ||||
| 	renderChart(); | ||||
| }); | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										201
									
								
								packages/frontend/src/pages/user/activity.pv.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								packages/frontend/src/pages/user/activity.pv.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | |||
| <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 } from 'chart.js'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import * as os from '@/os'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
| 
 | ||||
| const chartEl = $ref<HTMLCanvasElement>(null); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| const chartLimit = 30; | ||||
| let fetching = $ref(true); | ||||
| 
 | ||||
| const { handler: externalTooltipHandler } = useChartTooltip(); | ||||
| 
 | ||||
| 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) => ({ | ||||
| 			x: getDate(i).getTime(), | ||||
| 			y: v, | ||||
| 		})); | ||||
| 	}; | ||||
| 
 | ||||
| 	const raw = await os.api('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
| 
 | ||||
| 	const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||
| 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||
| 
 | ||||
| 	// フォントカラー | ||||
| 	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 
 | ||||
| 	const colorUser = '#3498db'; | ||||
| 	const colorVisitor = '#2ecc71'; | ||||
| 
 | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [{ | ||||
| 				parsing: false, | ||||
| 				label: 'UPV (user)', | ||||
| 				data: format(raw.upv.user).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorUser, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 1, | ||||
| 				fill: true, | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'UPV (visitor)', | ||||
| 				data: format(raw.upv.visitor).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorVisitor, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 1, | ||||
| 				fill: true, | ||||
| 			}], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 2.5, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
| 					right: 8, | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 				}, | ||||
| 			}, | ||||
| 			scales: { | ||||
| 				x: { | ||||
| 					type: 'time', | ||||
| 					offset: true, | ||||
| 					stacked: true, | ||||
| 					time: { | ||||
| 						stepSize: 1, | ||||
| 						unit: 'day', | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 						color: gridColor, | ||||
| 						borderColor: 'rgb(0, 0, 0, 0)', | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						maxRotation: 0, | ||||
| 						autoSkipPadding: 8, | ||||
| 					}, | ||||
| 					adapters: { | ||||
| 						date: { | ||||
| 							locale: enUS, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				y: { | ||||
| 					position: 'left', | ||||
| 					stacked: true, | ||||
| 					suggestedMax: 10, | ||||
| 					grid: { | ||||
| 						display: true, | ||||
| 						color: gridColor, | ||||
| 						borderColor: 'rgb(0, 0, 0, 0)', | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						//mirror: true, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			interaction: { | ||||
| 				intersect: false, | ||||
| 				mode: 'index', | ||||
| 			}, | ||||
| 			animation: false, | ||||
| 			plugins: { | ||||
| 				title: { | ||||
| 					display: true, | ||||
| 					text: 'Unique PV', | ||||
| 					padding: { | ||||
| 						left: 0, | ||||
| 						right: 0, | ||||
| 						top: 0, | ||||
| 						bottom: 12, | ||||
| 					}, | ||||
| 				}, | ||||
| 				legend: { | ||||
| 					display: true, | ||||
| 					position: 'bottom', | ||||
| 					padding: { | ||||
| 						left: 0, | ||||
| 						right: 0, | ||||
| 						top: 8, | ||||
| 						bottom: 0, | ||||
| 					}, | ||||
| 				}, | ||||
| 				tooltip: { | ||||
| 					enabled: false, | ||||
| 					mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 				gradient, | ||||
| 			}, | ||||
| 		}, | ||||
| 		plugins: [chartVLine(vLineColor)], | ||||
| 	}); | ||||
| 
 | ||||
| 	fetching = false; | ||||
| } | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 20px; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										29
									
								
								packages/frontend/src/pages/user/activity.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								packages/frontend/src/pages/user/activity.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<MkFolder class="item"> | ||||
| 		<template #header>Heatmap</template> | ||||
| 		<XHeatmap :user="user" :src="'notes'"/> | ||||
| 	</MkFolder> | ||||
| 	<MkFolder class="item"> | ||||
| 		<template #header>PV</template> | ||||
| 		<XPv :user="user"/> | ||||
| 	</MkFolder> | ||||
| </MkSpacer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XHeatmap from './activity.heatmap.vue'; | ||||
| import XPv from './activity.pv.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| 
 | ||||
| </style> | ||||
|  | @ -33,10 +33,16 @@ let chartSrc = $ref('per-user-notes'); | |||
| function showMenu(ev: MouseEvent) { | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.notes, | ||||
| 		active: true, | ||||
| 		active: chartSrc === 'per-user-notes', | ||||
| 		action: () => { | ||||
| 			chartSrc = 'per-user-notes'; | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.numberOfProfileView, | ||||
| 		active: chartSrc === 'per-user-pv', | ||||
| 		action: () => { | ||||
| 			chartSrc = 'per-user-pv'; | ||||
| 		}, | ||||
| 	}, /*, { | ||||
| 		text: i18n.ts.following, | ||||
| 		action: () => { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| 		<Transition name="fade" mode="out-in"> | ||||
| 			<div v-if="user"> | ||||
| 				<XHome v-if="tab === 'home'" :user="user"/> | ||||
| 				<XActivity v-else-if="tab === 'activity'" :user="user"/> | ||||
| 				<XReactions v-else-if="tab === 'reactions'" :user="user"/> | ||||
| 				<XClips v-else-if="tab === 'clips'" :user="user"/> | ||||
| 				<XPages v-else-if="tab === 'pages'" :user="user"/> | ||||
|  | @ -32,6 +33,7 @@ import { i18n } from '@/i18n'; | |||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| const XHome = defineAsyncComponent(() => import('./home.vue')); | ||||
| const XActivity = defineAsyncComponent(() => import('./activity.vue')); | ||||
| const XReactions = defineAsyncComponent(() => import('./reactions.vue')); | ||||
| const XClips = defineAsyncComponent(() => import('./clips.vue')); | ||||
| const XPages = defineAsyncComponent(() => import('./pages.vue')); | ||||
|  | @ -70,6 +72,10 @@ const headerTabs = $computed(() => user ? [{ | |||
| 	key: 'home', | ||||
| 	title: i18n.ts.overview, | ||||
| 	icon: 'ti ti-home', | ||||
| }, { | ||||
| 	key: 'activity', | ||||
| 	title: i18n.ts.activity, | ||||
| 	icon: 'ti ti-chart-line', | ||||
| }, ...($i && ($i.id === user.id)) || user.publicReactions ? [{ | ||||
| 	key: 'reactions', | ||||
| 	title: i18n.ts.reaction, | ||||
|  |  | |||
							
								
								
									
										44
									
								
								packages/frontend/src/scripts/init-chart.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/frontend/src/scripts/init-chart.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	DoughnutController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import zoomPlugin from 'chartjs-plugin-zoom'; | ||||
| import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; | ||||
| 
 | ||||
| export function initChart() { | ||||
| 	Chart.register( | ||||
| 		ArcElement, | ||||
| 		LineElement, | ||||
| 		BarElement, | ||||
| 		PointElement, | ||||
| 		BarController, | ||||
| 		LineController, | ||||
| 		DoughnutController, | ||||
| 		CategoryScale, | ||||
| 		LinearScale, | ||||
| 		TimeScale, | ||||
| 		Legend, | ||||
| 		Title, | ||||
| 		Tooltip, | ||||
| 		SubTitle, | ||||
| 		Filler, | ||||
| 		MatrixController, MatrixElement, | ||||
| 		zoomPlugin, | ||||
| 		gradient, | ||||
| 	); | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue