diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1d8b09b1c7..f1271c809f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -526,6 +526,7 @@ _widgets:
   trends: "トレンド"
   clock: "時計"
   rss: "RSSリーダー"
+  activity: "アクティビティ"
 
 _cw:
   hide: "隠す"
diff --git a/src/client/app.vue b/src/client/app.vue
index a23b6e1289..e88979f001 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -606,7 +606,8 @@ export default Vue.extend({
 				'calendar',
 				'rss',
 				'trends',
-				'clock'
+				'clock',
+				'activity',
 			];
 
 			this.$root.menu({
diff --git a/src/client/widgets/activity.calendar.vue b/src/client/widgets/activity.calendar.vue
new file mode 100644
index 0000000000..dfc0d29d3b
--- /dev/null
+++ b/src/client/widgets/activity.calendar.vue
@@ -0,0 +1,84 @@
+<template>
+<svg viewBox="0 0 21 7">
+	<rect v-for="record in data" class="day"
+		width="1" height="1"
+		:x="record.x" :y="record.date.weekday"
+		rx="1" ry="1"
+		fill="transparent">
+		<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
+	</rect>
+	<rect v-for="record in data" class="day"
+		:width="record.v" :height="record.v"
+		:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
+		rx="1" ry="1"
+		:fill="record.color"
+		style="pointer-events: none;"/>
+	<rect class="today"
+		width="1" height="1"
+		:x="data[0].x" :y="data[0].date.weekday"
+		rx="1" ry="1"
+		fill="none"
+		stroke-width="0.1"
+		stroke="#f73520"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['data'],
+	created() {
+		for (const d of this.data) {
+			d.total = d.notes + d.replies + d.renotes;
+		}
+		const peak = Math.max.apply(null, this.data.map(d => d.total));
+
+		const now = new Date();
+		const year = now.getFullYear();
+		const month = now.getMonth();
+		const day = now.getDate();
+
+		let x = 20;
+		this.data.slice().forEach((d, i) => {
+			d.x = x;
+
+			const date = new Date(year, month, day - i);
+			d.date = {
+				year: date.getFullYear(),
+				month: date.getMonth(),
+				day: date.getDate(),
+				weekday: date.getDay()
+			};
+
+			d.v = peak == 0 ? 0 : d.total / (peak / 2);
+			if (d.v > 1) d.v = 1;
+			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
+			const cs = d.v * 100;
+			const cl = 15 + ((1 - d.v) * 80);
+			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
+
+			if (d.date.weekday == 0) x--;
+		});
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+svg {
+	display: block;
+	padding: 16px;
+	width: 100%;
+	box-sizing: border-box;
+
+	> rect {
+		transform-origin: center;
+
+		&.day {
+			&:hover {
+				fill: rgba(#000, 0.05);
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/widgets/activity.chart.vue b/src/client/widgets/activity.chart.vue
new file mode 100644
index 0000000000..0278e02ae7
--- /dev/null
+++ b/src/client/widgets/activity.chart.vue
@@ -0,0 +1,108 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
+	<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>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+}
+
+export default Vue.extend({
+	i18n,
+	props: ['data'],
+	data() {
+		return {
+			viewBoxX: 147,
+			viewBoxY: 60,
+			zoom: 1,
+			pos: 0,
+			pointsNote: null,
+			pointsReply: null,
+			pointsRenote: null,
+			pointsTotal: null
+		};
+	},
+	created() {
+		for (const d of this.data) {
+			d.total = d.notes + d.replies + d.renotes;
+		}
+
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.data.map(d => d.total));
+			if (peak != 0) {
+				const data = this.data.slice().reverse();
+				this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+			}
+		},
+		onMousedown(e) {
+			const clickX = e.clientX;
+			const clickY = e.clientY;
+			const baseZoom = this.zoom;
+			const basePos = this.pos;
+
+			// 動かした時
+			dragListen(me => {
+				let moveLeft = me.clientX - clickX;
+				let moveTop = me.clientY - clickY;
+
+				this.zoom = baseZoom + (-moveTop / 20);
+				this.pos = basePos + moveLeft;
+				if (this.zoom < 1) this.zoom = 1;
+				if (this.pos > 0) this.pos = 0;
+				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
+
+				this.render();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+svg {
+	display: block;
+	padding: 16px;
+	width: 100%;
+	box-sizing: border-box;
+	cursor: all-scroll;
+}
+</style>
diff --git a/src/client/widgets/activity.vue b/src/client/widgets/activity.vue
new file mode 100644
index 0000000000..5f18c17d48
--- /dev/null
+++ b/src/client/widgets/activity.vue
@@ -0,0 +1,80 @@
+<template>
+<div>
+	<mk-container :show-header="props.design === 0" :naked="props.design === 2">
+		<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
+		<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
+
+		<div class="">
+			<mk-loading v-if="fetching"/>
+			<template v-else>
+				<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/>
+				<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
+			</template>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import { faChartBar, faSort } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import define from './define';
+import i18n from '../i18n';
+import XCalendar from './activity.calendar.vue';
+import XChart from './activity.chart.vue';
+
+export default define({
+	name: 'activity',
+	props: () => ({
+		design: 0,
+		view: 0
+	})
+}).extend({
+	i18n,
+	components: {
+		MkContainer,
+		XCalendar,
+		XChart,
+	},
+	data() {
+		return {
+			fetching: true,
+			activity: null,
+			faChartBar, faSort
+		};
+	},
+	mounted() {
+		this.$root.api('charts/user/notes', {
+			userId: this.$store.state.i.id,
+			span: 'day',
+			limit: 7 * 21
+		}).then(activity => {
+			this.activity = activity.diffs.normal.map((_, i) => ({
+				total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
+				notes: activity.diffs.normal[i],
+				replies: activity.diffs.reply[i],
+				renotes: activity.diffs.renote[i]
+			}));
+			this.fetching = false;
+		});
+	},
+	methods: {
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+			this.save();
+		},
+		toggleView() {
+			if (this.props.view == 1) {
+				this.props.view = 0;
+			} else {
+				this.props.view++;
+			}
+			this.save();
+		}
+	}
+});
+</script>
diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts
index d6af41e2f8..9f8bbc8882 100644
--- a/src/client/widgets/index.ts
+++ b/src/client/widgets/index.ts
@@ -7,3 +7,4 @@ Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default
 Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
 Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
 Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
+Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));