diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue
index 3c537d8d6d..97f05571c3 100644
--- a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue
@@ -1,11 +1,14 @@
 <template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-	<polyline
-		:points="points"
-		fill="none"
-		stroke-width="1"
-		stroke="#555"/>
-</svg>
+<div>
+	<a @click="span = 'day'">Per day</a> | <a @click="span = 'hour'">Per hour</a>
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<polyline
+			:points="points"
+			fill="none"
+			stroke-width="0.3"
+			stroke="#555"/>
+	</svg>
+</div>
 </template>
 
 <script lang="ts">
@@ -23,20 +26,40 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			viewBoxX: 365,
-			viewBoxY: 70,
-			points: null
+			viewBoxX: 100,
+			viewBoxY: 30,
+			points: null,
+			span: 'day'
 		};
 	},
-	created() {
-		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize));
+	computed: {
+		stats(): any[] {
+			return (
+				this.span == 'day' ? this.chart.perDay :
+				this.span == 'hour' ? this.chart.perHour :
+				null
+			);
+		}
+	},
+	watch: {
+		stats() {
+			this.render();
+		}
+	},
+	mounted() {
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.stats.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize));
 
-		if (peak != 0) {
-			const data = this.chart.slice().reverse().map(x => ({
-				size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize
-			}));
+			if (peak != 0) {
+				const data = this.stats.slice().reverse().map(x => ({
+					size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize
+				}));
 
-			this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' ');
+				this.points = data.map((d, i) => `${(this.viewBoxX / data.length) * i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' ');
+			}
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue
index 83c61c1313..fabb3f1bd1 100644
--- a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue
@@ -1,27 +1,30 @@
 <template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-	<polyline
-		:points="pointsNote"
-		fill="none"
-		stroke-width="1"
-		stroke="#41ddde"/>
-	<polyline
-		:points="pointsReply"
-		fill="none"
-		stroke-width="1"
-		stroke="#f7796c"/>
-	<polyline
-		:points="pointsRenote"
-		fill="none"
-		stroke-width="1"
-		stroke="#a1de41"/>
-	<polyline
-		:points="pointsTotal"
-		fill="none"
-		stroke-width="1"
-		stroke="#555"
-		stroke-dasharray="2 2"/>
-</svg>
+<div>
+	<a @click="span = 'day'">Per day</a> | <a @click="span = 'hour'">Per hour</a>
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<polyline
+			:points="pointsNote"
+			fill="none"
+			stroke-width="0.3"
+			stroke="#41ddde"/>
+		<polyline
+			:points="pointsReply"
+			fill="none"
+			stroke-width="0.3"
+			stroke="#f7796c"/>
+		<polyline
+			:points="pointsRenote"
+			fill="none"
+			stroke-width="0.3"
+			stroke="#a1de41"/>
+		<polyline
+			:points="pointsTotal"
+			fill="none"
+			stroke-width="0.3"
+			stroke="#555"
+			stroke-dasharray="1 1"/>
+	</svg>
+</div>
 </template>
 
 <script lang="ts">
@@ -39,29 +42,49 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			viewBoxX: 365,
-			viewBoxY: 70,
+			viewBoxX: 100,
+			viewBoxY: 30,
 			pointsNote: null,
 			pointsReply: null,
 			pointsRenote: null,
-			pointsTotal: null
+			pointsTotal: null,
+			span: 'day'
 		};
 	},
-	created() {
-		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff));
+	computed: {
+		stats(): any[] {
+			return (
+				this.span == 'day' ? this.chart.perDay :
+				this.span == 'hour' ? this.chart.perHour :
+				null
+			);
+		}
+	},
+	watch: {
+		stats() {
+			this.render();
+		}
+	},
+	mounted() {
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.stats.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff));
 
-		if (peak != 0) {
-			const data = this.chart.slice().reverse().map(x => ({
-				normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal,
-				reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply,
-				renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote,
-				total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff
-			}));
+			if (peak != 0) {
+				const data = this.stats.slice().reverse().map(x => ({
+					normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal,
+					reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply,
+					renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote,
+					total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff
+				}));
 
-			this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' ');
-			this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' ');
-			this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' ');
-			this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsNote = data.map((d, i) => `${(this.viewBoxX / data.length) * i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsReply = data.map((d, i) => `${(this.viewBoxX / data.length) * i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsRenote = data.map((d, i) => `${(this.viewBoxX / data.length) * i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsTotal = data.map((d, i) => `${(this.viewBoxX / data.length) * i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+			}
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue
index c2ab4a78e3..45ecc13929 100644
--- a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue
@@ -1,11 +1,14 @@
 <template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-	<polyline
-		:points="points"
-		fill="none"
-		stroke-width="1"
-		stroke="#555"/>
-</svg>
+<div>
+	<a @click="span = 'day'">Per day</a> | <a @click="span = 'hour'">Per hour</a>
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<polyline
+			:points="points"
+			fill="none"
+			stroke-width="0.3"
+			stroke="#555"/>
+	</svg>
+</div>
 </template>
 
 <script lang="ts">
@@ -23,20 +26,40 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			viewBoxX: 365,
-			viewBoxY: 70,
-			points: null
+			viewBoxX: 100,
+			viewBoxY: 30,
+			points: null,
+			span: 'day'
 		};
 	},
-	created() {
-		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff));
+	computed: {
+		stats(): any[] {
+			return (
+				this.span == 'day' ? this.chart.perDay :
+				this.span == 'hour' ? this.chart.perHour :
+				null
+			);
+		}
+	},
+	watch: {
+		stats() {
+			this.render();
+		}
+	},
+	mounted() {
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.stats.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff));
 
-		if (peak != 0) {
-			const data = this.chart.slice().reverse().map(x => ({
-				count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff
-			}));
+			if (peak != 0) {
+				const data = this.stats.slice().reverse().map(x => ({
+					count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff
+				}));
 
-			this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
+				this.points = data.map((d, i) => `${(this.viewBoxX / data.length) * i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
+			}
 		}
 	}
 });
diff --git a/src/server/api/endpoints/admin/chart.ts b/src/server/api/endpoints/admin/chart.ts
index c351c7167d..1897879d65 100644
--- a/src/server/api/endpoints/admin/chart.ts
+++ b/src/server/api/endpoints/admin/chart.ts
@@ -8,96 +8,130 @@ export const meta = {
 };
 
 export default (params: any) => new Promise(async (res, rej) => {
+	const daysRange = 365;
+	const hoursRange = 24;
+
 	const now = new Date();
 	const y = now.getFullYear();
 	const m = now.getMonth();
 	const d = now.getDate();
+	const h = now.getHours();
 
-	const stats = await Stats.find({
-		span: 'day',
-		date: {
-			$gt: new Date(y - 1, m, d)
-		}
-	}, {
-		sort: {
-			date: -1
-		},
-		fields: {
-			_id: 0
-		}
-	});
+	const [statsPerDay, statsPerHour] = await Promise.all([
+		Stats.find({
+			span: 'day',
+			date: {
+				$gt: new Date(y, m, d - daysRange)
+			}
+		}, {
+			sort: {
+				date: -1
+			},
+			fields: {
+				_id: 0
+			}
+		}),
+		Stats.find({
+			span: 'hour',
+			date: {
+				$gt: new Date(y, m, d, h - hoursRange)
+			}
+		}, {
+			sort: {
+				date: -1
+			},
+			fields: {
+				_id: 0
+			}
+		}),
+	]);
 
-	const chart: Array<Omit<IStats, '_id'>> = [];
+	const format = (src: IStats[], span: 'day' | 'hour') => {
+		const chart: Array<Omit<Omit<IStats, '_id'>, 'span'>> = [];
 
-	for (let i = 364; i >= 0; i--) {
-		const day = new Date(y, m, d - i);
+		const range =
+			span == 'day' ? daysRange :
+			span == 'hour' ? hoursRange :
+			null;
 
-		const stat = stats.find(s => s.date.getTime() == day.getTime());
+		for (let i = (range - 1); i >= 0; i--) {
+			const current =
+				span == 'day' ? new Date(y, m, d - i) :
+				span == 'hour' ? new Date(y, m, d, h - i) :
+				null;
 
-		if (stat) {
-			chart.unshift(stat);
-		} else { // 隙間埋め
-			const mostRecent = stats.find(s => s.date.getTime() < day.getTime());
-			if (mostRecent) {
-				chart.unshift(Object.assign({}, mostRecent, {
-					date: day
-				}));
-			} else {
-				chart.unshift({
-					date: day,
-					span: 'day',
-					users: {
-						local: {
-							total: 0,
-							diff: 0
-						},
-						remote: {
-							total: 0,
-							diff: 0
-						}
-					},
-					notes: {
-						local: {
-							total: 0,
-							diff: 0,
-							diffs: {
-								normal: 0,
-								reply: 0,
-								renote: 0
+			const stat = src.find(s => s.date.getTime() == current.getTime());
+
+			if (stat) {
+				chart.unshift(stat);
+			} else { // 隙間埋め
+				const mostRecent = src.find(s => s.date.getTime() < current.getTime());
+				if (mostRecent) {
+					chart.unshift(Object.assign({}, mostRecent, {
+						date: current
+					}));
+				} else {
+					chart.unshift({
+						date: current,
+						users: {
+							local: {
+								total: 0,
+								diff: 0
+							},
+							remote: {
+								total: 0,
+								diff: 0
 							}
 						},
-						remote: {
-							total: 0,
-							diff: 0,
-							diffs: {
-								normal: 0,
-								reply: 0,
-								renote: 0
+						notes: {
+							local: {
+								total: 0,
+								diff: 0,
+								diffs: {
+									normal: 0,
+									reply: 0,
+									renote: 0
+								}
+							},
+							remote: {
+								total: 0,
+								diff: 0,
+								diffs: {
+									normal: 0,
+									reply: 0,
+									renote: 0
+								}
+							}
+						},
+						drive: {
+							local: {
+								totalCount: 0,
+								totalSize: 0,
+								diffCount: 0,
+								diffSize: 0
+							},
+							remote: {
+								totalCount: 0,
+								totalSize: 0,
+								diffCount: 0,
+								diffSize: 0
 							}
 						}
-					},
-					drive: {
-						local: {
-							totalCount: 0,
-							totalSize: 0,
-							diffCount: 0,
-							diffSize: 0
-						},
-						remote: {
-							totalCount: 0,
-							totalSize: 0,
-							diffCount: 0,
-							diffSize: 0
-						}
-					}
-				});
+					});
+				}
 			}
 		}
-	}
 
-	chart.forEach(x => {
-		delete x.date;
+		chart.forEach(x => {
+			delete x.date;
+			delete (x as any).span;
+		});
+
+		return chart;
+	};
+
+	res({
+		perDay: format(statsPerDay, 'day'),
+		perHour: format(statsPerHour, 'hour')
 	});
-
-	res(chart);
 });