From 7a953392964f883c3b4c92cab165557f091090d6 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 2 Jan 2023 10:18:47 +0900 Subject: [PATCH] enhance(client): user activity page --- locales/ja-JP.yml | 1 + packages/frontend/src/components/MkChart.vue | 69 +++--- .../frontend/src/components/MkHeatmap.vue | 37 +-- .../src/components/MkInstanceStats.vue | 38 +-- .../src/components/MkRetentionHeatmap.vue | 37 +-- packages/frontend/src/pages/admin/metrics.vue | 32 +-- .../src/pages/admin/overview.active-users.vue | 37 +-- .../src/pages/admin/overview.ap-requests.vue | 37 +-- .../frontend/src/pages/admin/overview.pie.vue | 38 +-- .../src/pages/admin/overview.queue.chart.vue | 36 +-- .../src/pages/admin/queue.chart.chart.vue | 36 +-- .../src/pages/user/activity.heatmap.vue | 217 ++++++++++++++++++ .../frontend/src/pages/user/activity.pv.vue | 201 ++++++++++++++++ packages/frontend/src/pages/user/activity.vue | 29 +++ .../src/pages/user/index.activity.vue | 8 +- packages/frontend/src/pages/user/index.vue | 6 + packages/frontend/src/scripts/init-chart.ts | 44 ++++ 17 files changed, 564 insertions(+), 339 deletions(-) create mode 100644 packages/frontend/src/pages/user/activity.heatmap.vue create mode 100644 packages/frontend/src/pages/user/activity.pv.vue create mode 100644 packages/frontend/src/pages/user/activity.vue create mode 100644 packages/frontend/src/scripts/init-chart.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a07fb5ff91..eb30eed53a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -916,6 +916,7 @@ caption: "ã‚ャプション" loggedInAsBot: "Botアカウントã§ãƒã‚°ã‚¤ãƒ³ä¸" tools: "ツール" cannotLoad: "èªã¿è¾¼ã‚ã¾ã›ã‚“" +numberOfProfileView: "プãƒãƒ•ã‚£ãƒ¼ãƒ«è¡¨ç¤ºå›žæ•°" _sensitiveMediaDetection: description: "機械å¦ç¿’を使ã£ã¦è‡ªå‹•ã§ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚’検出ã—ã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã«å½¹ç«‹ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚サーãƒãƒ¼ã®è² è·ãŒå°‘ã—増ãˆã¾ã™ã€‚" diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index d99a5478e9..9ca7deaf80 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -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(); diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 078d0721da..5350928bfe 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -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; diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 382aaf16ef..e576caf78a 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -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'); diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index 547fe70a8c..b42c4f29a5 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -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); diff --git a/packages/frontend/src/pages/admin/metrics.vue b/packages/frontend/src/pages/admin/metrics.vue index 6c4803fe0b..f32b52d30a 100644 --- a/packages/frontend/src/pages/admin/metrics.vue +++ b/packages/frontend/src/pages/admin/metrics.vue @@ -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: { diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue index ea8c74f3a2..190635c754 100644 --- a/packages/frontend/src/pages/admin/overview.active-users.vue +++ b/packages/frontend/src/pages/admin/overview.active-users.vue @@ -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(); diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index d15507564d..fa6a6f30d3 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -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>(); diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue index 94509cf006..33ab6fe851 100644 --- a/packages/frontend/src/pages/admin/overview.pie.vue +++ b/packages/frontend/src/pages/admin/overview.pie.vue @@ -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 }[]; diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue index 2552e0a6c3..bb51ffd68f 100644 --- a/packages/frontend/src/pages/admin/overview.queue.chart.vue +++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue @@ -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; diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue index b91689589d..f95cd1c872 100644 --- a/packages/frontend/src/pages/admin/queue.chart.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue @@ -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; diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue new file mode 100644 index 0000000000..86e3a0f4f9 --- /dev/null +++ b/packages/frontend/src/pages/user/activity.heatmap.vue @@ -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> diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue new file mode 100644 index 0000000000..d709bc01b9 --- /dev/null +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -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> diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue new file mode 100644 index 0000000000..f9dce3a9e8 --- /dev/null +++ b/packages/frontend/src/pages/user/activity.vue @@ -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> diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue index 523072d2e6..0cc1524663 100644 --- a/packages/frontend/src/pages/user/index.activity.vue +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -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: () => { diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index f40cd0b8d6..b60cef3729 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -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, diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts new file mode 100644 index 0000000000..32f887f2e7 --- /dev/null +++ b/packages/frontend/src/scripts/init-chart.ts @@ -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, + ); +} -- GitLab