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;