Migrate to Chart.js v3 (#7896)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* 定期的にresync

* Update overview.vue

* wip

* wip
This commit is contained in:
syuilo 2021-10-22 05:36:48 +09:00 committed by GitHub
parent e7660bc8db
commit 4e4c559db6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 980 additions and 1103 deletions

View file

@ -0,0 +1,628 @@
<template>
<canvas ref="chartEl"></canvas>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch, PropType } 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';
import * as os from '@client/os';
import { defaultStore } from '@client/store';
Chart.register(
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
zoomPlugin,
);
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 = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
const getColor = (i) => {
return colors[i % colors.length];
};
export default defineComponent({
props: {
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
},
},
setup(props) {
const now = new Date();
let chartInstance: Chart = null;
let data: {
series: {
name: string;
type: 'line' | 'area';
color?: string;
borderDash?: number[];
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 render = () => {
if (chartInstance) {
chartInstance.destroy();
}
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');
chartInstance = new Chart(chartEl.value, {
type: 'line',
data: {
labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
datasets: data.series.map((x, i) => ({
parsing: false,
label: x.name,
data: x.data.slice().reverse(),
pointRadius: 0,
tension: 0,
borderWidth: 2,
borderColor: x.color ? x.color : getColor(i),
borderDash: x.borderDash || [],
borderJoinStyle: 'round',
backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
fill: x.type === 'area',
hidden: !!x.hidden,
})),
},
options: {
aspectRatio: 2.5,
layout: {
padding: {
left: 16,
right: 16,
top: 16,
bottom: 8,
},
},
scales: {
x: {
type: 'time',
time: {
stepSize: 1,
unit: props.span === 'day' ? 'month' : 'day',
},
grid: {
display: props.detailed,
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
},
ticks: {
display: props.detailed,
},
adapters: {
date: {
locale: enUS,
},
},
min: getDate(props.limit).getTime(),
},
y: {
position: 'left',
grid: {
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
},
ticks: {
display: props.detailed,
},
},
},
interaction: {
intersect: false,
},
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
},
},
tooltip: {
mode: 'index',
animation: {
duration: 0,
},
},
zoom: {
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',
},
}
},
},
},
});
};
const exportData = () => {
// TODO
};
const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Instances',
type: 'area',
data: format(total
? raw.instance.total
: sum(raw.instance.inc, negate(raw.instance.dec))
),
}],
};
};
const fetchNotesChart = async (type: string): Promise<typeof data> => {
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
return {
series: [{
name: 'All',
type: 'line',
borderDash: [5, 5],
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))
),
}, {
name: 'Renotes',
type: 'area',
data: format(type == 'combined'
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
: raw[type].diffs.renote
),
}, {
name: 'Replies',
type: 'area',
data: format(type == 'combined'
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
: raw[type].diffs.reply
),
}, {
name: 'Normal',
type: 'area',
data: format(type == 'combined'
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
: raw[type].diffs.normal
),
}],
};
};
const fetchNotesTotalChart = async (): Promise<typeof data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Combined',
type: 'line',
data: format(sum(raw.local.users, raw.remote.users)),
}, {
name: 'Local',
type: 'area',
data: format(raw.local.users),
}, {
name: 'Remote',
type: 'area',
data: format(raw.remote.users),
}],
};
};
const fetchDriveChart = async (): Promise<typeof data> => {
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
return {
bytes: true,
series: [{
name: 'All',
type: 'line',
borderDash: [5, 5],
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 fetchDriveTotalChart = async (): Promise<typeof data> => {
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
return {
bytes: true,
series: [{
name: 'Combined',
type: 'line',
data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
}, {
name: 'Local',
type: 'area',
data: format(raw.local.totalSize),
}, {
name: 'Remote',
type: 'area',
data: format(raw.remote.totalSize),
}],
};
};
const fetchDriveFilesChart = async (): Promise<typeof data> => {
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
return {
series: [{
name: 'All',
type: 'line',
borderDash: [5, 5],
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 fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Combined',
type: 'line',
data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
}, {
name: 'Local',
type: 'area',
data: format(raw.local.totalCount),
}, {
name: 'Remote',
type: 'area',
data: format(raw.remote.totalCount),
}],
};
};
const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('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 data> => {
const raw = await os.api('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 fetchAndRender = async () => {
const fetchData = () => {
switch (props.src) {
case 'federation-instances': return fetchFederationInstancesChart(false);
case 'federation-instances-total': return fetchFederationInstancesChart(true);
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-total': return fetchDriveTotalChart();
case 'drive-files': return fetchDriveFilesChart();
case 'drive-files-total': return fetchDriveFilesTotalChart();
case 'instances-requests': return fetchInstanceRequestsChart();
case 'instances-users': return fetchInstanceUsersChart(false);
case 'instances-users-total': return fetchInstanceUsersChart(true);
case 'instances-notes': return fetchInstanceNotesChart(false);
case 'instances-notes-total': return fetchInstanceNotesChart(true);
case 'instances-ff': return fetchInstanceFfChart(false);
case 'instances-ff-total': return fetchInstanceFfChart(true);
case 'instances-drive-usage': return fetchInstanceDriveUsageChart(false);
case 'instances-drive-usage-total': return fetchInstanceDriveUsageChart(true);
case 'instances-drive-files': return fetchInstanceDriveFilesChart(false);
case 'instances-drive-files-total': return fetchInstanceDriveFilesChart(true);
}
};
fetching.value = true;
data = await fetchData();
fetching.value = false;
render();
};
watch(() => [props.src, props.span], fetchAndRender);
onMounted(() => {
fetchAndRender();
});
return {
chartEl,
};
},
});
</script>

View file

@ -24,35 +24,26 @@
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0;">
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
<option value="hour">{{ $ts.perHour }}</option>
<option value="day">{{ $ts.perDay }}</option>
</MkSelect>
</div>
<canvas ref="chart"></canvas>
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import Chart from 'chart.js';
import MkSelect from './form/select.vue';
import number from '@client/filters/number';
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})`;
};
import { defineComponent, onMounted, ref, watch } from 'vue';
import MkSelect from '@client/components/form/select.vue';
import MkChart from '@client/components/chart.vue';
import * as os from '@client/os';
import { defaultStore } from '@client/store';
export default defineComponent({
components: {
MkSelect
MkSelect,
MkChart,
},
props: {
@ -68,463 +59,15 @@ export default defineComponent({
},
},
data() {
setup() {
const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref('notes');
return {
notesLocalWoW: 0,
notesLocalDoD: 0,
notesRemoteWoW: 0,
notesRemoteDoD: 0,
usersLocalWoW: 0,
usersLocalDoD: 0,
usersRemoteWoW: 0,
usersRemoteDoD: 0,
now: null,
chart: null,
chartInstance: null,
chartSrc: 'notes',
chartSpan: 'hour',
}
chartSrc,
chartSpan,
};
},
computed: {
data(): any {
if (this.chart == null) return null;
switch (this.chartSrc) {
case 'federation-instances': return this.federationInstancesChart(false);
case 'federation-instances-total': return this.federationInstancesChart(true);
case 'users': return this.usersChart(false);
case 'users-total': return this.usersChart(true);
case 'active-users': return this.activeUsersChart();
case 'notes': return this.notesChart('combined');
case 'local-notes': return this.notesChart('local');
case 'remote-notes': return this.notesChart('remote');
case 'notes-total': return this.notesTotalChart();
case 'drive': return this.driveChart();
case 'drive-total': return this.driveTotalChart();
case 'drive-files': return this.driveFilesChart();
case 'drive-files-total': return this.driveFilesTotalChart();
}
},
stats(): any[] {
const stats =
this.chartSpan == 'day' ? this.chart.perDay :
this.chartSpan == 'hour' ? this.chart.perHour :
null;
return stats;
}
},
watch: {
chartSrc() {
this.renderChart();
},
chartSpan() {
this.renderChart();
}
},
async created() {
this.now = new Date();
this.fetchChart();
},
methods: {
async fetchChart() {
const [perHour, perDay] = await Promise.all([Promise.all([
os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
]), Promise.all([
os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
])]);
const chart = {
perHour: {
federation: perHour[0],
users: perHour[1],
activeUsers: perHour[2],
notes: perHour[3],
drive: perHour[4],
},
perDay: {
federation: perDay[0],
users: perDay[1],
activeUsers: perDay[2],
notes: perDay[3],
drive: perDay[4],
}
};
this.chart = chart;
this.renderChart();
},
renderChart() {
if (this.chartInstance) {
this.chartInstance.destroy();
}
// TODO: var(--panel)
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chartInstance = markRaw(new Chart(this.$refs.chart, {
type: 'line',
data: {
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
datasets: this.data.series.map(x => ({
label: x.name,
data: x.data.slice().reverse(),
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: x.color,
borderDash: x.borderDash || [],
backgroundColor: alpha(x.color, 0.1),
fill: x.fill == null ? true : x.fill,
hidden: !!x.hidden
}))
},
options: {
aspectRatio: 2.5,
layout: {
padding: {
left: 16,
right: 16,
top: 16,
bottom: 8
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
type: 'time',
time: {
stepSize: 1,
unit: this.chartSpan == 'day' ? 'month' : 'day',
},
gridLines: {
display: this.detailed,
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: this.detailed
}
}],
yAxes: [{
position: 'left',
gridLines: {
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: this.detailed
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
}));
},
getDate(ago: number) {
const y = this.now.getFullYear();
const m = this.now.getMonth();
const d = this.now.getDate();
const h = this.now.getHours();
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
},
format(arr) {
const now = Date.now();
return arr.map((v, i) => ({
x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
y: v
}));
},
federationInstancesChart(total: boolean): any {
return {
series: [{
name: 'Instances',
color: '#008FFB',
data: this.format(total
? this.stats.federation.instance.total
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
)
}]
};
},
notesChart(type: string): any {
return {
series: [{
name: 'All',
type: 'line',
color: '#008FFB',
borderDash: [5, 5],
fill: false,
data: this.format(type == 'combined'
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
)
}, {
name: 'Renotes',
type: 'area',
color: '#00E396',
data: this.format(type == 'combined'
? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
: this.stats.notes[type].diffs.renote
)
}, {
name: 'Replies',
type: 'area',
color: '#FEB019',
data: this.format(type == 'combined'
? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
: this.stats.notes[type].diffs.reply
)
}, {
name: 'Normal',
type: 'area',
color: '#FF4560',
data: this.format(type == 'combined'
? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
: this.stats.notes[type].diffs.normal
)
}]
};
},
notesTotalChart(): any {
return {
series: [{
name: 'Combined',
type: 'line',
color: '#008FFB',
data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
}, {
name: 'Local',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.notes.local.total)
}, {
name: 'Remote',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.notes.remote.total)
}]
};
},
usersChart(total: boolean): any {
return {
series: [{
name: 'Combined',
type: 'line',
color: '#008FFB',
data: this.format(total
? sum(this.stats.users.local.total, this.stats.users.remote.total)
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
)
}, {
name: 'Local',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(total
? this.stats.users.local.total
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
)
}, {
name: 'Remote',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(total
? this.stats.users.remote.total
: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
)
}]
};
},
activeUsersChart(): any {
return {
series: [{
name: 'Combined',
type: 'line',
color: '#008FFB',
data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
}, {
name: 'Local',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.activeUsers.local.count)
}, {
name: 'Remote',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.activeUsers.remote.count)
}]
};
},
driveChart(): any {
return {
bytes: true,
series: [{
name: 'All',
type: 'line',
color: '#09d8e2',
borderDash: [5, 5],
fill: false,
data: this.format(
sum(
this.stats.drive.local.incSize,
negate(this.stats.drive.local.decSize),
this.stats.drive.remote.incSize,
negate(this.stats.drive.remote.decSize)
)
)
}, {
name: 'Local +',
type: 'area',
color: '#008FFB',
data: this.format(this.stats.drive.local.incSize)
}, {
name: 'Local -',
type: 'area',
color: '#FF4560',
data: this.format(negate(this.stats.drive.local.decSize))
}, {
name: 'Remote +',
type: 'area',
color: '#00E396',
data: this.format(this.stats.drive.remote.incSize)
}, {
name: 'Remote -',
type: 'area',
color: '#FEB019',
data: this.format(negate(this.stats.drive.remote.decSize))
}]
};
},
driveTotalChart(): any {
return {
bytes: true,
series: [{
name: 'Combined',
type: 'line',
color: '#008FFB',
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
}, {
name: 'Local',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.drive.local.totalSize)
}, {
name: 'Remote',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.drive.remote.totalSize)
}]
};
},
driveFilesChart(): any {
return {
series: [{
name: 'All',
type: 'line',
color: '#09d8e2',
borderDash: [5, 5],
fill: false,
data: this.format(
sum(
this.stats.drive.local.incCount,
negate(this.stats.drive.local.decCount),
this.stats.drive.remote.incCount,
negate(this.stats.drive.remote.decCount)
)
)
}, {
name: 'Local +',
type: 'area',
color: '#008FFB',
data: this.format(this.stats.drive.local.incCount)
}, {
name: 'Local -',
type: 'area',
color: '#FF4560',
data: this.format(negate(this.stats.drive.local.decCount))
}, {
name: 'Remote +',
type: 'area',
color: '#00E396',
data: this.format(this.stats.drive.remote.incCount)
}, {
name: 'Remote -',
type: 'area',
color: '#FEB019',
data: this.format(negate(this.stats.drive.remote.decCount))
}]
};
},
driveFilesTotalChart(): any {
return {
series: [{
name: 'Combined',
type: 'line',
color: '#008FFB',
data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
}, {
name: 'Local',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.drive.local.totalCount)
}, {
name: 'Remote',
type: 'area',
color: '#008FFB',
hidden: true,
data: this.format(this.stats.drive.remote.totalCount)
}]
};
},
number
}
});
</script>

View file

@ -0,0 +1,47 @@
<template>
<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
<slot name="before"></slot>{{ isPlus ? '+' : isMinus ? '-' : '' }}{{ number(value) }}<slot name="after"></slot>
</span>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import number from '@client/filters/number';
export default defineComponent({
props: {
value: {
type: Number,
required: true
},
},
setup(props) {
const isPlus = computed(() => props.value > 0);
const isMinus = computed(() => props.value < 0);
const isZero = computed(() => props.value === 0);
return {
isPlus,
isMinus,
isZero,
number,
};
}
});
</script>
<style lang="scss" scoped>
.ceaaebcd {
&.isPlus {
color: var(--success);
}
&.isMinus {
color: var(--error);
}
&.isZero {
opacity: 0.5;
}
}
</style>