diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index f47b680f8340d87c0cf2de8cb19edcaf26109c74..a77f3627f964ed67c7bc12ec395bfbcb49efc69a 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -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',
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 157608965762f63ced523b1657b2455c22f54d2d..00f5e96286996b63661d4fabedded4c5b7646c70 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -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>();
 
diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue
deleted file mode 100644
index ea3276a890c7a2ade1dc853ddccd6e94d25621c5..0000000000000000000000000000000000000000
--- a/packages/frontend/src/pages/user/activity.heatmap.vue
+++ /dev/null
@@ -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>
diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue
index 6703890893e5637f7153dc5d67291f932fcffa54..3c7635a312e29ab62ed3e19214225acda539bbba 100644
--- a/packages/frontend/src/pages/user/activity.vue
+++ b/packages/frontend/src/pages/user/activity.vue
@@ -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;