mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	refactor(frontend): activity.heatmap.vueをコンポーネントに置換 (#12967)
				
					
				
			This commit is contained in:
		
							parent
							
								
									d2063df78d
								
							
						
					
					
						commit
						1aeede97f5
					
				
					 4 changed files with 28 additions and 231 deletions
				
			
		| 
						 | 
				
			
			@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -23,9 +24,16 @@ import { initChart } from '@/scripts/init-chart.js';
 | 
			
		|||
 | 
			
		||||
initChart();
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	src: string;
 | 
			
		||||
}>();
 | 
			
		||||
export type HeatmapSource = 'active-users' | 'notes' | 'ap-requests-inbox-received' | 'ap-requests-deliver-succeeded' | 'ap-requests-deliver-failed';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	src: HeatmapSource;
 | 
			
		||||
	user?: Misskey.entities.User;
 | 
			
		||||
	label?: string;
 | 
			
		||||
}>(), {
 | 
			
		||||
	user: undefined,
 | 
			
		||||
	label: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const rootEl = shallowRef<HTMLDivElement>(null);
 | 
			
		||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -75,8 +83,13 @@ async function renderChart() {
 | 
			
		|||
		const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
 | 
			
		||||
		values = raw.readWrite;
 | 
			
		||||
	} else if (props.src === 'notes') {
 | 
			
		||||
		const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' });
 | 
			
		||||
		values = raw.local.inc;
 | 
			
		||||
		if (props.user) {
 | 
			
		||||
			const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
 | 
			
		||||
			values = raw.inc;
 | 
			
		||||
		} else {
 | 
			
		||||
			const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' });
 | 
			
		||||
			values = raw.local.inc;
 | 
			
		||||
		}
 | 
			
		||||
	} else if (props.src === 'ap-requests-inbox-received') {
 | 
			
		||||
		const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
 | 
			
		||||
		values = raw.inboxReceived;
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +118,7 @@ async function renderChart() {
 | 
			
		|||
		type: 'matrix',
 | 
			
		||||
		data: {
 | 
			
		||||
			datasets: [{
 | 
			
		||||
				label: 'Read & Write',
 | 
			
		||||
				label: props.label,
 | 
			
		||||
				data: format(values),
 | 
			
		||||
				pointRadius: 0,
 | 
			
		||||
				borderWidth: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +141,9 @@ async function renderChart() {
 | 
			
		|||
					const a = c.chart.chartArea ?? {};
 | 
			
		||||
					return (a.bottom - a.top) / 7 - marginEachCell;
 | 
			
		||||
				},
 | 
			
		||||
			/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
			
		||||
			}] satisfies ChartData[],
 | 
			
		||||
			 */
 | 
			
		||||
			}],
 | 
			
		||||
		},
 | 
			
		||||
		options: {
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +211,7 @@ async function renderChart() {
 | 
			
		|||
						},
 | 
			
		||||
						label(context) {
 | 
			
		||||
							const v = context.dataset.data[context.dataIndex];
 | 
			
		||||
							return ['Active: ' + v.v];
 | 
			
		||||
							return [v.v];
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					//mode: 'index',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<div class="_panel" :class="$style.heatmap">
 | 
			
		||||
			<MkHeatmap :src="heatmapSrc"/>
 | 
			
		||||
			<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkFoldableSection>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +92,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
 | 
			
		|||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import MkHeatmap from '@/components/MkHeatmap.vue';
 | 
			
		||||
import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue';
 | 
			
		||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
 | 
			
		||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
 | 
			
		||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +103,7 @@ initChart();
 | 
			
		|||
const chartLimit = 500;
 | 
			
		||||
const chartSpan = ref<'hour' | 'day'>('hour');
 | 
			
		||||
const chartSrc = ref('active-users');
 | 
			
		||||
const heatmapSrc = ref('active-users');
 | 
			
		||||
const heatmapSrc = ref<HeatmapSource>('active-users');
 | 
			
		||||
const subDoughnutEl = shallowRef<HTMLCanvasElement>();
 | 
			
		||||
const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,219 +0,0 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div ref="rootEl">
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<div v-else :class="$style.root" class="_panel">
 | 
			
		||||
		<canvas ref="chartEl"></canvas>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
 | 
			
		||||
import { alpha } from '@/scripts/color.js';
 | 
			
		||||
import { initChart } from '@/scripts/init-chart.js';
 | 
			
		||||
 | 
			
		||||
initChart();
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	src: string;
 | 
			
		||||
	user: Misskey.entities.User;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const rootEl = shallowRef<HTMLDivElement>(null);
 | 
			
		||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
 | 
			
		||||
const now = new Date();
 | 
			
		||||
let chartInstance: Chart = null;
 | 
			
		||||
const fetching = ref(true);
 | 
			
		||||
 | 
			
		||||
const { handler: externalTooltipHandler } = useChartTooltip({
 | 
			
		||||
	position: 'middle',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function renderChart() {
 | 
			
		||||
	if (chartInstance) {
 | 
			
		||||
		chartInstance.destroy();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const wide = rootEl.value.offsetWidth > 700;
 | 
			
		||||
	const narrow = rootEl.value.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 misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
 | 
			
		||||
		values = raw.inc;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fetching.value = false;
 | 
			
		||||
 | 
			
		||||
	await nextTick();
 | 
			
		||||
 | 
			
		||||
	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.value, {
 | 
			
		||||
		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;
 | 
			
		||||
				},
 | 
			
		||||
			/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
			
		||||
			}] satisfies ChartData[],
 | 
			
		||||
			 */
 | 
			
		||||
			}],
 | 
			
		||||
		},
 | 
			
		||||
		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: {
 | 
			
		||||
							day: 'M/d',
 | 
			
		||||
							month: 'Y/M',
 | 
			
		||||
							week: 'M/d',
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: true,
 | 
			
		||||
						maxRotation: 0,
 | 
			
		||||
						autoSkipPadding: 8,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				y: {
 | 
			
		||||
					offset: true,
 | 
			
		||||
					reverse: true,
 | 
			
		||||
					position: 'right',
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						maxRotation: 0,
 | 
			
		||||
						autoSkip: true,
 | 
			
		||||
						padding: 1,
 | 
			
		||||
						font: {
 | 
			
		||||
							size: 9,
 | 
			
		||||
						},
 | 
			
		||||
						callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value],
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			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.value = true;
 | 
			
		||||
	renderChart();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	renderChart();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	<div class="_gaps">
 | 
			
		||||
		<MkFoldableSection class="item">
 | 
			
		||||
			<template #header><i class="ti ti-activity"></i> Heatmap</template>
 | 
			
		||||
			<XHeatmap :user="user" :src="'notes'"/>
 | 
			
		||||
			<MkHeatmap :user="user" :src="'notes'"/>
 | 
			
		||||
		</MkFoldableSection>
 | 
			
		||||
		<MkFoldableSection class="item">
 | 
			
		||||
			<template #header><i class="ti ti-pencil"></i> Notes</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XHeatmap from './activity.heatmap.vue';
 | 
			
		||||
import XPv from './activity.pv.vue';
 | 
			
		||||
import XNotes from './activity.notes.vue';
 | 
			
		||||
import XFollowing from './activity.following.vue';
 | 
			
		||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
 | 
			
		||||
import MkHeatmap from '@/components/MkHeatmap.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	user: Misskey.entities.User;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue