diff options
Diffstat (limited to 'packages/client/src/components/MkChart.vue')
| -rw-r--r-- | packages/client/src/components/MkChart.vue | 880 |
1 files changed, 0 insertions, 880 deletions
diff --git a/packages/client/src/components/MkChart.vue b/packages/client/src/components/MkChart.vue deleted file mode 100644 index 31e95404fa..0000000000 --- a/packages/client/src/components/MkChart.vue +++ /dev/null @@ -1,880 +0,0 @@ -<template> -<div class="cbbedffa"> - <canvas ref="chartEl"></canvas> - <div v-if="fetching" class="fetching"> - <MkLoading/> - </div> -</div> -</template> - -<script lang="ts" setup> -/* eslint-disable id-denylist -- - Chart.js has a `data` attribute in most chart definitions, which triggers the - id-denylist violation when setting it. This is causing about 60+ lint issues. - 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 'chartjs-adapter-date-fns'; -import { enUS } from 'date-fns/locale'; -import zoomPlugin from 'chartjs-plugin-zoom'; -// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114242002 -// We can't use gradient because Vite throws a error. -//import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os'; -import { defaultStore } from '@/store'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip'; - -const props = defineProps({ - src: { - type: String, - required: true, - }, - args: { - type: Object, - required: false, - }, - limit: { - type: Number, - required: false, - default: 90, - }, - span: { - type: String as PropType<'hour' | 'day'>, - required: true, - }, - detailed: { - type: Boolean, - required: false, - default: false, - }, - stacked: { - type: Boolean, - required: false, - default: false, - }, - bar: { - type: Boolean, - required: false, - default: false, - }, - aspectRatio: { - type: Number, - required: false, - default: null, - }, -}); - -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); -const alpha = (hex, a) => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; - -const colors = { - blue: '#008FFB', - green: '#00E396', - yellow: '#FEB019', - red: '#FF4560', - purple: '#e300db', - orange: '#fe6919', - lime: '#bde800', - cyan: '#00e0e0', -}; -const colorSets = [colors.blue, colors.green, colors.yellow, colors.red, colors.purple]; -const getColor = (i) => { - return colorSets[i % colorSets.length]; -}; - -const now = new Date(); -let chartInstance: Chart = null; -let chartData: { - series: { - name: string; - type: 'line' | 'area'; - color?: string; - dashed?: boolean; - hidden?: boolean; - data: { - x: number; - y: number; - }[]; - }[]; -} = null; - -const chartEl = ref<HTMLCanvasElement>(null); -const fetching = ref(true); - -const getDate = (ago: number) => { - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - const h = now.getHours(); - - return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); -}; - -const format = (arr) => { - return arr.map((v, i) => ({ - x: getDate(i).getTime(), - y: v, - })); -}; - -const { handler: externalTooltipHandler } = useChartTooltip(); - -const render = () => { - if (chartInstance) { - chartInstance.destroy(); - } - - 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 maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); - - chartInstance = new Chart(chartEl.value, { - type: props.bar ? 'bar' : 'line', - data: { - labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(), - datasets: chartData.series.map((x, i) => ({ - parsing: false, - label: x.name, - data: x.data.slice().reverse(), - tension: 0.3, - pointRadius: 0, - borderWidth: props.bar ? 0 : 2, - borderColor: x.color ? x.color : getColor(i), - borderDash: x.dashed ? [5, 5] : [], - borderJoinStyle: 'round', - borderRadius: props.bar ? 3 : undefined, - backgroundColor: props.bar ? (x.color ? x.color : getColor(i)) : alpha(x.color ? x.color : getColor(i), 0.1), - /*gradient: props.bar ? undefined : { - backgroundColor: { - axis: 'y', - colors: { - 0: alpha(x.color ? x.color : getColor(i), 0), - [maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2), - }, - }, - },*/ - barPercentage: 0.9, - categoryPercentage: 0.9, - fill: x.type === 'area', - clip: 8, - hidden: !!x.hidden, - })), - }, - options: { - aspectRatio: props.aspectRatio || 2.5, - layout: { - padding: { - left: 0, - right: 8, - top: 0, - bottom: 0, - }, - }, - scales: { - x: { - type: 'time', - stacked: props.stacked, - offset: false, - time: { - stepSize: 1, - unit: props.span === 'day' ? 'month' : 'day', - }, - grid: { - color: gridColor, - borderColor: 'rgb(0, 0, 0, 0)', - }, - ticks: { - display: props.detailed, - maxRotation: 0, - autoSkipPadding: 16, - }, - adapters: { - date: { - locale: enUS, - }, - }, - min: getDate(props.limit).getTime(), - }, - y: { - position: 'left', - stacked: props.stacked, - suggestedMax: 50, - grid: { - color: gridColor, - borderColor: 'rgb(0, 0, 0, 0)', - }, - ticks: { - display: props.detailed, - //mirror: true, - }, - }, - }, - interaction: { - intersect: false, - mode: 'index', - }, - elements: { - point: { - hoverRadius: 5, - hoverBorderWidth: 2, - }, - }, - animation: false, - plugins: { - legend: { - display: props.detailed, - position: 'bottom', - labels: { - boxWidth: 16, - }, - }, - tooltip: { - enabled: false, - mode: 'index', - animation: { - duration: 0, - }, - external: externalTooltipHandler, - }, - zoom: props.detailed ? { - pan: { - enabled: true, - }, - zoom: { - wheel: { - enabled: true, - }, - pinch: { - enabled: true, - }, - drag: { - enabled: false, - }, - mode: 'x', - }, - limits: { - x: { - min: 'original', - max: 'original', - }, - y: { - min: 'original', - max: 'original', - }, - }, - } : undefined, - //gradient, - }, - }, - plugins: [{ - id: 'vLine', - beforeDraw(chart, args, options) { - if (chart.tooltip?._active?.length) { - const activePoint = chart.tooltip._active[0]; - const ctx = chart.ctx; - const x = activePoint.element.x; - const topY = chart.scales.y.top; - const bottomY = chart.scales.y.bottom; - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(x, bottomY); - ctx.lineTo(x, topY); - ctx.lineWidth = 1; - ctx.strokeStyle = vLineColor; - ctx.stroke(); - ctx.restore(); - } - }, - }], - }); -}; - -const exportData = () => { - // TODO -}; - -const fetchFederationChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/federation', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Received', - type: 'area', - data: format(raw.inboxInstances), - color: colors.blue, - }, { - name: 'Delivered', - type: 'area', - data: format(raw.deliveredInstances), - color: colors.green, - }, { - name: 'Stalled', - type: 'area', - data: format(raw.stalled), - color: colors.red, - }, { - name: 'Pub Active', - type: 'line', - data: format(raw.pubActive), - color: colors.purple, - }, { - name: 'Sub Active', - type: 'line', - data: format(raw.subActive), - color: colors.orange, - }, { - name: 'Pub & Sub', - type: 'line', - data: format(raw.pubsub), - dashed: true, - color: colors.cyan, - }, { - name: 'Pub', - type: 'line', - data: format(raw.pub), - dashed: true, - color: colors.purple, - }, { - name: 'Sub', - type: 'line', - data: format(raw.sub), - dashed: true, - color: colors.orange, - }], - }; -}; - -const fetchApRequestChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/ap-request', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'In', - type: 'area', - color: '#008FFB', - data: format(raw.inboxReceived), - }, { - name: 'Out (succ)', - type: 'area', - color: '#00E396', - data: format(raw.deliverSucceeded), - }, { - name: 'Out (fail)', - type: 'area', - color: '#FEB019', - data: format(raw.deliverFailed), - }], - }; -}; - -const fetchNotesChart = async (type: string): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'All', - type: 'line', - data: format(type === 'combined' - ? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) - : sum(raw[type].inc, negate(raw[type].dec)), - ), - color: '#888888', - }, { - name: 'Renotes', - type: 'area', - data: format(type === 'combined' - ? sum(raw.local.diffs.renote, raw.remote.diffs.renote) - : raw[type].diffs.renote, - ), - color: colors.green, - }, { - name: 'Replies', - type: 'area', - data: format(type === 'combined' - ? sum(raw.local.diffs.reply, raw.remote.diffs.reply) - : raw[type].diffs.reply, - ), - color: colors.yellow, - }, { - name: 'Normal', - type: 'area', - data: format(type === 'combined' - ? sum(raw.local.diffs.normal, raw.remote.diffs.normal) - : raw[type].diffs.normal, - ), - color: colors.blue, - }, { - name: 'With file', - type: 'area', - data: format(type === 'combined' - ? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) - : raw[type].diffs.withFile, - ), - color: colors.purple, - }], - }; -}; - -const fetchNotesTotalChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Combined', - type: 'line', - data: format(sum(raw.local.total, raw.remote.total)), - }, { - name: 'Local', - type: 'area', - data: format(raw.local.total), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.total), - }], - }; -}; - -const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/users', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Combined', - type: 'line', - data: format(total - ? sum(raw.local.total, raw.remote.total) - : sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)), - ), - }, { - name: 'Local', - type: 'area', - data: format(total - ? raw.local.total - : sum(raw.local.inc, negate(raw.local.dec)), - ), - }, { - name: 'Remote', - type: 'area', - data: format(total - ? raw.remote.total - : sum(raw.remote.inc, negate(raw.remote.dec)), - ), - }], - }; -}; - -const fetchActiveUsersChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/active-users', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Read & Write', - type: 'area', - data: format(raw.readWrite), - color: colors.orange, - }, { - name: 'Write', - type: 'area', - data: format(raw.write), - color: colors.lime, - }, { - name: 'Read', - type: 'area', - data: format(raw.read), - color: colors.blue, - }, { - name: '< Week', - type: 'area', - data: format(raw.registeredWithinWeek), - color: colors.green, - }, { - name: '< Month', - type: 'area', - data: format(raw.registeredWithinMonth), - color: colors.yellow, - }, { - name: '< Year', - type: 'area', - data: format(raw.registeredWithinYear), - color: colors.red, - }, { - name: '> Week', - type: 'area', - data: format(raw.registeredOutsideWeek), - color: colors.yellow, - }, { - name: '> Month', - type: 'area', - data: format(raw.registeredOutsideMonth), - color: colors.red, - }, { - name: '> Year', - type: 'area', - data: format(raw.registeredOutsideYear), - color: colors.purple, - }], - }; -}; - -const fetchDriveChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span }); - return { - bytes: true, - series: [{ - name: 'All', - type: 'line', - dashed: true, - data: format( - sum( - raw.local.incSize, - negate(raw.local.decSize), - raw.remote.incSize, - negate(raw.remote.decSize), - ), - ), - }, { - name: 'Local +', - type: 'area', - data: format(raw.local.incSize), - }, { - name: 'Local -', - type: 'area', - data: format(negate(raw.local.decSize)), - }, { - name: 'Remote +', - type: 'area', - data: format(raw.remote.incSize), - }, { - name: 'Remote -', - type: 'area', - data: format(negate(raw.remote.decSize)), - }], - }; -}; - -const fetchDriveFilesChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'All', - type: 'line', - dashed: true, - data: format( - sum( - raw.local.incCount, - negate(raw.local.decCount), - raw.remote.incCount, - negate(raw.remote.decCount), - ), - ), - }, { - name: 'Local +', - type: 'area', - data: format(raw.local.incCount), - }, { - name: 'Local -', - type: 'area', - data: format(negate(raw.local.decCount)), - }, { - name: 'Remote +', - type: 'area', - data: format(raw.remote.incCount), - }, { - name: 'Remote -', - type: 'area', - data: format(negate(raw.remote.decCount)), - }], - }; -}; - -const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'In', - type: 'area', - color: '#008FFB', - data: format(raw.requests.received), - }, { - name: 'Out (succ)', - type: 'area', - color: '#00E396', - data: format(raw.requests.succeeded), - }, { - name: 'Out (fail)', - type: 'area', - color: '#FEB019', - data: format(raw.requests.failed), - }], - }; -}; - -const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Users', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.users.total - : sum(raw.users.inc, negate(raw.users.dec)), - ), - }], - }; -}; - -const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Notes', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.notes.total - : sum(raw.notes.inc, negate(raw.notes.dec)), - ), - }], - }; -}; - -const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Following', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.following.total - : sum(raw.following.inc, negate(raw.following.dec)), - ), - }, { - name: 'Followers', - type: 'area', - color: '#00E396', - data: format(total - ? raw.followers.total - : sum(raw.followers.inc, negate(raw.followers.dec)), - ), - }], - }; -}; - -const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - bytes: true, - series: [{ - name: 'Drive usage', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.drive.totalUsage - : sum(raw.drive.incUsage, negate(raw.drive.decUsage)), - ), - }], - }; -}; - -const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Drive files', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.drive.totalFiles - : sum(raw.drive.incFiles, negate(raw.drive.decFiles)), - ), - }], - }; -}; - -const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); - return { - series: [...(props.args.withoutAll ? [] : [{ - name: 'All', - type: 'line', - data: format(sum(raw.inc, negate(raw.dec))), - color: '#888888', - }]), { - name: 'With file', - type: 'area', - data: format(raw.diffs.withFile), - color: colors.purple, - }, { - name: 'Renotes', - type: 'area', - data: format(raw.diffs.renote), - color: colors.green, - }, { - name: 'Replies', - type: 'area', - data: format(raw.diffs.reply), - color: colors.yellow, - }, { - name: 'Normal', - type: 'area', - data: format(raw.diffs.normal), - 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 { - series: [{ - name: 'Local', - type: 'area', - data: format(raw.local.followings.total), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.followings.total), - }], - }; -}; - -const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Local', - type: 'area', - data: format(raw.local.followers.total), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.followers.total), - }], - }; -}; - -const fetchPerUserDriveChart = async (): Promise<typeof chartData> => { - const raw = await os.apiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Inc', - type: 'area', - data: format(raw.incSize), - }, { - name: 'Dec', - type: 'area', - data: format(raw.decSize), - }], - }; -}; - -const fetchAndRender = async () => { - const fetchData = () => { - switch (props.src) { - case 'federation': return fetchFederationChart(); - case 'ap-request': return fetchApRequestChart(); - case 'users': return fetchUsersChart(false); - case 'users-total': return fetchUsersChart(true); - case 'active-users': return fetchActiveUsersChart(); - case 'notes': return fetchNotesChart('combined'); - case 'local-notes': return fetchNotesChart('local'); - case 'remote-notes': return fetchNotesChart('remote'); - case 'notes-total': return fetchNotesTotalChart(); - case 'drive': return fetchDriveChart(); - case 'drive-files': return fetchDriveFilesChart(); - case 'instance-requests': return fetchInstanceRequestsChart(); - case 'instance-users': return fetchInstanceUsersChart(false); - case 'instance-users-total': return fetchInstanceUsersChart(true); - case 'instance-notes': return fetchInstanceNotesChart(false); - case 'instance-notes-total': return fetchInstanceNotesChart(true); - case 'instance-ff': return fetchInstanceFfChart(false); - case 'instance-ff-total': return fetchInstanceFfChart(true); - case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false); - case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true); - case 'instance-drive-files': return fetchInstanceDriveFilesChart(false); - case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); - - case 'per-user-notes': return fetchPerUserNotesChart(); - case 'per-user-following': return fetchPerUserFollowingChart(); - case 'per-user-followers': return fetchPerUserFollowersChart(); - case 'per-user-drive': return fetchPerUserDriveChart(); - } - }; - fetching.value = true; - chartData = await fetchData(); - fetching.value = false; - render(); -}; - -watch(() => [props.src, props.span], fetchAndRender); - -onMounted(() => { - fetchAndRender(); -}); -/* eslint-enable id-denylist */ -</script> - -<style lang="scss" scoped> -.cbbedffa { - position: relative; - - > .fetching { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - -webkit-backdrop-filter: var(--blur, blur(12px)); - backdrop-filter: var(--blur, blur(12px)); - display: flex; - justify-content: center; - align-items: center; - cursor: wait; - } -} -</style> |