summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-05-12 10:29:27 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-05-12 10:29:27 +0900
commit055dc6bb669234d3dc45845476ecb4fbd736734a (patch)
treecd0041d3f2878200ec9fb05478364de5812ad298 /packages/frontend/src/components
parent:art: (diff)
downloadsharkey-055dc6bb669234d3dc45845476ecb4fbd736734a.tar.gz
sharkey-055dc6bb669234d3dc45845476ecb4fbd736734a.tar.bz2
sharkey-055dc6bb669234d3dc45845476ecb4fbd736734a.zip
enhance(frontend): add retention line chart
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue13
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue4
-rw-r--r--packages/frontend/src/components/MkRetentionLineChart.vue130
3 files changed, 145 insertions, 2 deletions
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>