summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-01-02 10:18:47 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-01-02 10:18:47 +0900
commit7a953392964f883c3b4c92cab165557f091090d6 (patch)
tree5950d1555c0666564349e77aa47122de9804df8d
parentfix style (diff)
downloadmisskey-7a953392964f883c3b4c92cab165557f091090d6.tar.gz
misskey-7a953392964f883c3b4c92cab165557f091090d6.tar.bz2
misskey-7a953392964f883c3b4c92cab165557f091090d6.zip
enhance(client): user activity page
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/frontend/src/components/MkChart.vue69
-rw-r--r--packages/frontend/src/components/MkHeatmap.vue37
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue38
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue37
-rw-r--r--packages/frontend/src/pages/admin/metrics.vue32
-rw-r--r--packages/frontend/src/pages/admin/overview.active-users.vue37
-rw-r--r--packages/frontend/src/pages/admin/overview.ap-requests.vue37
-rw-r--r--packages/frontend/src/pages/admin/overview.pie.vue38
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.chart.vue36
-rw-r--r--packages/frontend/src/pages/admin/queue.chart.chart.vue36
-rw-r--r--packages/frontend/src/pages/user/activity.heatmap.vue217
-rw-r--r--packages/frontend/src/pages/user/activity.pv.vue201
-rw-r--r--packages/frontend/src/pages/user/activity.vue29
-rw-r--r--packages/frontend/src/pages/user/index.activity.vue8
-rw-r--r--packages/frontend/src/pages/user/index.vue6
-rw-r--r--packages/frontend/src/scripts/init-chart.ts44
17 files changed, 564 insertions, 339 deletions
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,
+ );
+}