enhance(client): user activity page

This commit is contained in:
syuilo 2023-01-02 10:18:47 +09:00
parent 1df23a839a
commit 7a95339296
17 changed files with 564 additions and 339 deletions

View File

@ -916,6 +916,7 @@ caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中" loggedInAsBot: "Botアカウントでログイン中"
tools: "ツール" tools: "ツール"
cannotLoad: "読み込めません" cannotLoad: "読み込めません"
numberOfProfileView: "プロフィール表示回数"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"

View File

@ -14,26 +14,9 @@
As this is part of Chart.js's API it makes sense to disable the check here. 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 { onMounted, ref, watch, PropType, onUnmounted } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import zoomPlugin from 'chartjs-plugin-zoom';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
@ -41,6 +24,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import date from '@/filters/date'; import date from '@/filters/date';
import { initChart } from '@/scripts/init-chart';
initChart();
const props = defineProps({ const props = defineProps({
src: { 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 sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x); 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 fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
return { return {
@ -814,6 +808,7 @@ const fetchAndRender = async () => {
case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
case 'per-user-notes': return fetchPerUserNotesChart(); case 'per-user-notes': return fetchPerUserNotesChart();
case 'per-user-pv': return fetchPerUserPvChart();
case 'per-user-following': return fetchPerUserFollowingChart(); case 'per-user-following': return fetchPerUserFollowingChart();
case 'per-user-followers': return fetchPerUserFollowersChart(); case 'per-user-followers': return fetchPerUserFollowersChart();
case 'per-user-drive': return fetchPerUserDriveChart(); case 'per-user-drive': return fetchPerUserDriveChart();

View File

@ -9,23 +9,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as os from '@/os'; import * as os from '@/os';
@ -35,24 +19,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
MatrixController, MatrixElement,
);
const props = defineProps<{ const props = defineProps<{
src: string; src: string;

View File

@ -77,24 +77,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
DoughnutController,
} from 'chart.js';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
@ -103,24 +86,9 @@ import { i18n } from '@/i18n';
import MkHeatmap from '@/components/MkHeatmap.vue'; import MkHeatmap from '@/components/MkHeatmap.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
DoughnutController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
);
const chartLimit = 500; const chartLimit = 500;
let chartSpan = $ref<'hour' | 'day'>('hour'); let chartSpan = $ref<'hour' | 'day'>('hour');

View File

@ -9,23 +9,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as os from '@/os'; import * as os from '@/os';
@ -35,24 +19,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
MatrixController, MatrixElement,
);
const rootEl = $ref<HTMLDivElement>(null); const rootEl = $ref<HTMLDivElement>(null);
const chartEl = $ref<HTMLCanvasElement>(null); const chartEl = $ref<HTMLCanvasElement>(null);

View File

@ -52,21 +52,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
Legend,
Title,
Tooltip,
SubTitle,
} from 'chart.js';
import MkwFederation from '../../widgets/federation.vue'; import MkwFederation from '../../widgets/federation.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
@ -79,21 +65,9 @@ import number from '@/filters/number';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
Legend,
Title,
Tooltip,
SubTitle,
);
export default defineComponent({ export default defineComponent({
components: { components: {

View File

@ -9,23 +9,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as os from '@/os'; import * as os from '@/os';
@ -35,24 +19,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
gradient,
);
const chartEl = $ref<HTMLCanvasElement>(null); const chartEl = $ref<HTMLCanvasElement>(null);
const now = new Date(); const now = new Date();

View File

@ -16,23 +16,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
@ -45,24 +29,9 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
gradient,
);
const chartLimit = 50; const chartLimit = 50;
const chartEl = $ref<HTMLCanvasElement>(); const chartEl = $ref<HTMLCanvasElement>();

View File

@ -4,45 +4,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
DoughnutController,
} from 'chart.js';
import number from '@/filters/number'; import number from '@/filters/number';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
DoughnutController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
);
const props = defineProps<{ const props = defineProps<{
data: { name: string; value: number; color: string; onClick?: () => void }[]; data: { name: string; value: number; color: string; onClick?: () => void }[];

View File

@ -4,46 +4,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watch, onMounted, onUnmounted, ref } from 'vue'; import { watch, onMounted, onUnmounted, ref } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import number from '@/filters/number'; import number from '@/filters/number';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
);
const props = defineProps<{ const props = defineProps<{
type: string; type: string;

View File

@ -4,46 +4,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watch, onMounted, onUnmounted, ref } from 'vue'; import { watch, onMounted, onUnmounted, ref } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import number from '@/filters/number'; import number from '@/filters/number';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
);
const props = defineProps<{ const props = defineProps<{
type: string; type: string;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -33,10 +33,16 @@ let chartSrc = $ref('per-user-notes');
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent) {
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.notes, text: i18n.ts.notes,
active: true, active: chartSrc === 'per-user-notes',
action: () => { action: () => {
chartSrc = 'per-user-notes'; chartSrc = 'per-user-notes';
}, },
}, {
text: i18n.ts.numberOfProfileView,
active: chartSrc === 'per-user-pv',
action: () => {
chartSrc = 'per-user-pv';
},
}, /*, { }, /*, {
text: i18n.ts.following, text: i18n.ts.following,
action: () => { action: () => {

View File

@ -5,6 +5,7 @@
<Transition name="fade" mode="out-in"> <Transition name="fade" mode="out-in">
<div v-if="user"> <div v-if="user">
<XHome v-if="tab === 'home'" :user="user"/> <XHome v-if="tab === 'home'" :user="user"/>
<XActivity v-else-if="tab === 'activity'" :user="user"/>
<XReactions v-else-if="tab === 'reactions'" :user="user"/> <XReactions v-else-if="tab === 'reactions'" :user="user"/>
<XClips v-else-if="tab === 'clips'" :user="user"/> <XClips v-else-if="tab === 'clips'" :user="user"/>
<XPages v-else-if="tab === 'pages'" :user="user"/> <XPages v-else-if="tab === 'pages'" :user="user"/>
@ -32,6 +33,7 @@ import { i18n } from '@/i18n';
import { $i } from '@/account'; import { $i } from '@/account';
const XHome = defineAsyncComponent(() => import('./home.vue')); const XHome = defineAsyncComponent(() => import('./home.vue'));
const XActivity = defineAsyncComponent(() => import('./activity.vue'));
const XReactions = defineAsyncComponent(() => import('./reactions.vue')); const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
const XClips = defineAsyncComponent(() => import('./clips.vue')); const XClips = defineAsyncComponent(() => import('./clips.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue'));
@ -70,6 +72,10 @@ const headerTabs = $computed(() => user ? [{
key: 'home', key: 'home',
title: i18n.ts.overview, title: i18n.ts.overview,
icon: 'ti ti-home', icon: 'ti ti-home',
}, {
key: 'activity',
title: i18n.ts.activity,
icon: 'ti ti-chart-line',
}, ...($i && ($i.id === user.id)) || user.publicReactions ? [{ }, ...($i && ($i.id === user.id)) || user.publicReactions ? [{
key: 'reactions', key: 'reactions',
title: i18n.ts.reaction, title: i18n.ts.reaction,

View File

@ -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,
);
}