Refactor Chart component (#8622)

* refactor(client): refactor Chart component

* Apply review suggestions from @Johann150

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>

* fix(client): don't expose values from Chart

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>
This commit is contained in:
Andy 2022-05-14 14:20:41 +02:00 committed by GitHub
parent e0cce893be
commit 88307327e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -7,8 +7,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; /* 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 { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue';
import { import {
Chart, Chart,
ArcElement, ArcElement,
@ -36,6 +41,46 @@ import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import MkChartTooltip from '@/components/chart-tooltip.vue'; import MkChartTooltip from '@/components/chart-tooltip.vue';
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( Chart.register(
ArcElement, ArcElement,
LineElement, LineElement,
@ -80,51 +125,9 @@ const getColor = (i) => {
return colorSets[i % colorSets.length]; return colorSets[i % colorSets.length];
}; };
export default defineComponent({ const now = new Date();
props: { let chartInstance: Chart = null;
src: { let chartData: {
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
},
},
setup(props) {
const now = new Date();
let chartInstance: Chart = null;
let data: {
series: { series: {
name: string; name: string;
type: 'line' | 'area'; type: 'line' | 'area';
@ -136,45 +139,45 @@ export default defineComponent({
y: number; y: number;
}[]; }[];
}[]; }[];
} = null; } = null;
const chartEl = ref<HTMLCanvasElement>(null); const chartEl = ref<HTMLCanvasElement>(null);
const fetching = ref(true); const fetching = ref(true);
const getDate = (ago: number) => { const getDate = (ago: number) => {
const y = now.getFullYear(); const y = now.getFullYear();
const m = now.getMonth(); const m = now.getMonth();
const d = now.getDate(); const d = now.getDate();
const h = now.getHours(); const h = now.getHours();
return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
}; };
const format = (arr) => { const format = (arr) => {
return arr.map((v, i) => ({ return arr.map((v, i) => ({
x: getDate(i).getTime(), x: getDate(i).getTime(),
y: v y: v
})); }));
}; };
const tooltipShowing = ref(false); const tooltipShowing = ref(false);
const tooltipX = ref(0); const tooltipX = ref(0);
const tooltipY = ref(0); const tooltipY = ref(0);
const tooltipTitle = ref(null); const tooltipTitle = ref(null);
const tooltipSeries = ref(null); const tooltipSeries = ref(null);
let disposeTooltipComponent; let disposeTooltipComponent;
os.popup(MkChartTooltip, { os.popup(MkChartTooltip, {
showing: tooltipShowing, showing: tooltipShowing,
x: tooltipX, x: tooltipX,
y: tooltipY, y: tooltipY,
title: tooltipTitle, title: tooltipTitle,
series: tooltipSeries, series: tooltipSeries,
}, {}).then(({ dispose }) => { }, {}).then(({ dispose }) => {
disposeTooltipComponent = dispose; disposeTooltipComponent = dispose;
}); });
function externalTooltipHandler(context) { function externalTooltipHandler(context) {
if (context.tooltip.opacity === 0) { if (context.tooltip.opacity === 0) {
tooltipShowing.value = false; tooltipShowing.value = false;
return; return;
@ -192,9 +195,9 @@ export default defineComponent({
tooltipShowing.value = true; tooltipShowing.value = true;
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
} }
const render = () => { const render = () => {
if (chartInstance) { if (chartInstance) {
chartInstance.destroy(); chartInstance.destroy();
} }
@ -205,13 +208,13 @@ export default defineComponent({
// //
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
const maxes = data.series.map((x, i) => Math.max(...x.data.map(d => d.y))); const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {
type: props.bar ? 'bar' : 'line', type: props.bar ? 'bar' : 'line',
data: { data: {
labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(), labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
datasets: data.series.map((x, i) => ({ datasets: chartData.series.map((x, i) => ({
parsing: false, parsing: false,
label: x.name, label: x.name,
data: x.data.slice().reverse(), data: x.data.slice().reverse(),
@ -367,13 +370,13 @@ export default defineComponent({
} }
}] }]
}); });
}; };
const exportData = () => { const exportData = () => {
// TODO // TODO
}; };
const fetchFederationChart = async (): Promise<typeof data> => { const fetchFederationChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -421,9 +424,9 @@ export default defineComponent({
color: colors.orange, color: colors.orange,
}], }],
}; };
}; };
const fetchApRequestChart = async (): Promise<typeof data> => { const fetchApRequestChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span }); const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -443,15 +446,15 @@ export default defineComponent({
data: format(raw.deliverFailed) data: format(raw.deliverFailed)
}] }]
}; };
}; };
const fetchNotesChart = async (type: string): Promise<typeof data> => { const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'All', name: 'All',
type: 'line', type: 'line',
data: format(type == 'combined' data: format(type === 'combined'
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) ? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
: sum(raw[type].inc, negate(raw[type].dec)) : sum(raw[type].inc, negate(raw[type].dec))
), ),
@ -459,7 +462,7 @@ export default defineComponent({
}, { }, {
name: 'Renotes', name: 'Renotes',
type: 'area', type: 'area',
data: format(type == 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.renote, raw.remote.diffs.renote) ? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
: raw[type].diffs.renote : raw[type].diffs.renote
), ),
@ -467,7 +470,7 @@ export default defineComponent({
}, { }, {
name: 'Replies', name: 'Replies',
type: 'area', type: 'area',
data: format(type == 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.reply, raw.remote.diffs.reply) ? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
: raw[type].diffs.reply : raw[type].diffs.reply
), ),
@ -475,7 +478,7 @@ export default defineComponent({
}, { }, {
name: 'Normal', name: 'Normal',
type: 'area', type: 'area',
data: format(type == 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.normal, raw.remote.diffs.normal) ? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
: raw[type].diffs.normal : raw[type].diffs.normal
), ),
@ -483,16 +486,16 @@ export default defineComponent({
}, { }, {
name: 'With file', name: 'With file',
type: 'area', type: 'area',
data: format(type == 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) ? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
: raw[type].diffs.withFile : raw[type].diffs.withFile
), ),
color: colors.purple, color: colors.purple,
}], }],
}; };
}; };
const fetchNotesTotalChart = async (): Promise<typeof data> => { const fetchNotesTotalChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -509,9 +512,9 @@ export default defineComponent({
data: format(raw.remote.total), data: format(raw.remote.total),
}], }],
}; };
}; };
const fetchUsersChart = async (total: boolean): Promise<typeof data> => { const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await os.api('charts/users', { limit: props.limit, span: props.span }); const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -537,9 +540,9 @@ export default defineComponent({
), ),
}], }],
}; };
}; };
const fetchActiveUsersChart = async (): Promise<typeof data> => { const fetchActiveUsersChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span }); const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -589,9 +592,9 @@ export default defineComponent({
color: colors.purple, color: colors.purple,
}], }],
}; };
}; };
const fetchDriveChart = async (): Promise<typeof data> => { const fetchDriveChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
return { return {
bytes: true, bytes: true,
@ -625,9 +628,9 @@ export default defineComponent({
data: format(negate(raw.remote.decSize)), data: format(negate(raw.remote.decSize)),
}], }],
}; };
}; };
const fetchDriveFilesChart = async (): Promise<typeof data> => { const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -660,9 +663,9 @@ export default defineComponent({
data: format(negate(raw.remote.decCount)), data: format(negate(raw.remote.decCount)),
}], }],
}; };
}; };
const fetchInstanceRequestsChart = async (): Promise<typeof data> => { const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -682,9 +685,9 @@ export default defineComponent({
data: format(raw.requests.failed) data: format(raw.requests.failed)
}] }]
}; };
}; };
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => { const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -697,9 +700,9 @@ export default defineComponent({
) )
}] }]
}; };
}; };
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => { const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -712,9 +715,9 @@ export default defineComponent({
) )
}] }]
}; };
}; };
const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => { const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -735,9 +738,9 @@ export default defineComponent({
) )
}] }]
}; };
}; };
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => { const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return { return {
bytes: true, bytes: true,
@ -751,9 +754,9 @@ export default defineComponent({
) )
}] }]
}; };
}; };
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => { const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -766,9 +769,9 @@ export default defineComponent({
) )
}] }]
}; };
}; };
const fetchPerUserNotesChart = async (): Promise<typeof data> => { const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
return { return {
series: [...(props.args.withoutAll ? [] : [{ series: [...(props.args.withoutAll ? [] : [{
@ -798,9 +801,9 @@ export default defineComponent({
color: colors.blue, color: colors.blue,
}], }],
}; };
}; };
const fetchPerUserFollowingChart = async (): Promise<typeof data> => { const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -813,9 +816,9 @@ export default defineComponent({
data: format(raw.remote.followings.total), data: format(raw.remote.followings.total),
}], }],
}; };
}; };
const fetchPerUserFollowersChart = async (): Promise<typeof data> => { const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -828,9 +831,9 @@ export default defineComponent({
data: format(raw.remote.followers.total), data: format(raw.remote.followers.total),
}], }],
}; };
}; };
const fetchPerUserDriveChart = async (): Promise<typeof data> => { const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
@ -843,9 +846,9 @@ export default defineComponent({
data: format(raw.decSize), data: format(raw.decSize),
}], }],
}; };
}; };
const fetchAndRender = async () => { const fetchAndRender = async () => {
const fetchData = () => { const fetchData = () => {
switch (props.src) { switch (props.src) {
case 'federation': return fetchFederationChart(); case 'federation': return fetchFederationChart();
@ -859,7 +862,6 @@ export default defineComponent({
case 'notes-total': return fetchNotesTotalChart(); case 'notes-total': return fetchNotesTotalChart();
case 'drive': return fetchDriveChart(); case 'drive': return fetchDriveChart();
case 'drive-files': return fetchDriveFilesChart(); case 'drive-files': return fetchDriveFilesChart();
case 'instance-requests': return fetchInstanceRequestsChart(); case 'instance-requests': return fetchInstanceRequestsChart();
case 'instance-users': return fetchInstanceUsersChart(false); case 'instance-users': return fetchInstanceUsersChart(false);
case 'instance-users-total': return fetchInstanceUsersChart(true); case 'instance-users-total': return fetchInstanceUsersChart(true);
@ -879,27 +881,21 @@ export default defineComponent({
} }
}; };
fetching.value = true; fetching.value = true;
data = await fetchData(); chartData = await fetchData();
fetching.value = false; fetching.value = false;
render(); render();
}; };
watch(() => [props.src, props.span], fetchAndRender); watch(() => [props.src, props.span], fetchAndRender);
onMounted(() => { onMounted(() => {
fetchAndRender(); fetchAndRender();
});
onUnmounted(() => {
if (disposeTooltipComponent) disposeTooltipComponent();
});
return {
chartEl,
fetching,
};
},
}); });
onUnmounted(() => {
if (disposeTooltipComponent) disposeTooltipComponent();
});
/* eslint-enable id-denylist */
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>