diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-05-12 10:29:27 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2023-05-12 10:29:27 +0900 |
| commit | 055dc6bb669234d3dc45845476ecb4fbd736734a (patch) | |
| tree | cd0041d3f2878200ec9fb05478364de5812ad298 | |
| parent | :art: (diff) | |
| download | sharkey-055dc6bb669234d3dc45845476ecb4fbd736734a.tar.gz sharkey-055dc6bb669234d3dc45845476ecb4fbd736734a.tar.bz2 sharkey-055dc6bb669234d3dc45845476ecb4fbd736734a.zip | |
enhance(frontend): add retention line chart
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkInstanceStats.vue | 13 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkRetentionHeatmap.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkRetentionLineChart.vue | 130 |
4 files changed, 146 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c38cb12c3b..eafed11eff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー ### Client - ユーザーを指定してのノート検索が可能に - アカウント初期設定ウィザードにプライバシー設定を追加 +- リテンション率チャートに折れ線グラフを追加 - Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 - Fix: カラーバーがリプライには表示されないのを修正 - Fix: チャンネル内の検索ボックスが挙動不審な問題を修正 diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 0f87fef6b1..6fcd8f7811 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -52,9 +52,12 @@ <MkFoldableSection class="item"> <template #header>Retention rate</template> - <div class="_panel" :class="$style.retention"> + <div class="_panel" :class="$style.retentionHeatmap"> <MkRetentionHeatmap/> </div> + <div class="_panel" :class="$style.retentionLine"> + <MkRetentionLineChart/> + </div> </MkFoldableSection> <MkFoldableSection class="item"> @@ -86,6 +89,7 @@ import { i18n } from '@/i18n'; import MkHeatmap from '@/components/MkHeatmap.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; +import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; import { initChart } from '@/scripts/init-chart'; initChart(); @@ -202,7 +206,12 @@ onMounted(() => { margin-bottom: 16px; } -.retention { +.retentionHeatmap { + padding: 16px; + margin-bottom: 16px; +} + +.retentionLine { padding: 16px; margin-bottom: 16px; } diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index 3f5f09f5d0..fc2bc5c87f 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -129,6 +129,10 @@ async function renderChart() { autoSkip: false, callback: (value, index, values) => value, }, + title: { + display: true, + text: 'Days later', + }, }, y: { type: 'time', diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue new file mode 100644 index 0000000000..8bd0279806 --- /dev/null +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -0,0 +1,130 @@ +<template> +<canvas ref="chartEl"></canvas> +</template> + +<script lang="ts" setup> +import { onMounted, shallowRef } from 'vue'; +import { Chart } from 'chart.js'; +import tinycolor from 'tinycolor2'; +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'; +import * as os from '@/os'; + +initChart(); + +const chartEl = shallowRef<HTMLCanvasElement>(null); + +const { handler: externalTooltipHandler } = useChartTooltip(); + +let chartInstance: Chart; + +const getYYYYMMDD = (date: Date) => { + const y = date.getFullYear().toString().padStart(2, '0'); + const m = (date.getMonth() + 1).toString().padStart(2, '0'); + const d = date.getDate().toString().padStart(2, '0'); + return `${y}/${m}/${d}`; +}; + +const getDate = (ymd: string) => { + const [y, m, d] = ymd.split('-').map(x => parseInt(x, 10)); + const date = new Date(y, m + 1, d, 0, 0, 0, 0); + return date; +}; + +onMounted(async () => { + let raw = await os.api('retention', { }); + + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + + const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); + const color = accent.toHex(); + + chartInstance = new Chart(chartEl.value, { + type: 'line', + data: { + labels: [], + datasets: raw.map((record, i) => ({ + label: getYYYYMMDD(new Date(record.createdAt)), + pointRadius: 0, + borderWidth: 2, + borderJoinStyle: 'round', + borderColor: alpha(color, Math.min(1, (raw.length - (i - 1)) / raw.length)), + fill: false, + tension: 0.4, + data: [{ + x: '0', + y: 100, + d: getYYYYMMDD(new Date(record.createdAt)), + }, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({ + x: (i + 1).toString(), + y: (v / record.users) * 100, + d: getYYYYMMDD(new Date(record.createdAt)), + }))], + })), + }, + options: { + aspectRatio: 2.5, + layout: { + padding: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + scales: { + x: { + title: { + display: true, + text: 'Days later', + }, + }, + y: { + title: { + display: true, + text: 'Rate (%)', + }, + ticks: { + callback: (value, index, values) => value + '%', + }, + }, + }, + interaction: { + intersect: false, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + callbacks: { + title(context) { + const v = context[0].dataset.data[context[0].dataIndex]; + return `${v.x} days later`; + }, + label(context) { + const v = context.dataset.data[context.dataIndex]; + const p = Math.round(v.y) + '%'; + return `${v.d} ${p}`; + }, + }, + mode: 'index', + animation: { + duration: 0, + }, + external: externalTooltipHandler, + }, + }, + }, + plugins: [chartVLine(vLineColor)], + }); +}); +</script> + +<style lang="scss" scoped> + +</style> |