From 7d7193cb63ac8811152707a6896e5cf7ae05258c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Jun 2018 11:24:29 +0900
Subject: [PATCH] :v:

---
 locales/ja.yml                                |  3 +
 .../common/views/widgets/hashtags.chart.vue   | 78 +++++++++++++++++++
 .../app/common/views/widgets/hashtags.vue     | 57 +++++++-------
 src/server/api/endpoints/hashtags/trend.ts    | 12 ++-
 4 files changed, 119 insertions(+), 31 deletions(-)
 create mode 100644 src/client/app/common/views/widgets/hashtags.chart.vue

diff --git a/locales/ja.yml b/locales/ja.yml
index 64ee00dba..da751c9e3 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -254,6 +254,9 @@ common/views/widgets/posts-monitor.vue:
   title: "投稿チャート"
   toggle: "表示を切り替え"
 
+common/views/widgets/hashtags.vue:
+  title: "ハッシュタグ"
+
 common/views/widgets/server.vue:
   title: "サーバー情報"
   toggle: "表示を切り替え"
diff --git a/src/client/app/common/views/widgets/hashtags.chart.vue b/src/client/app/common/views/widgets/hashtags.chart.vue
new file mode 100644
index 000000000..19b56ef28
--- /dev/null
+++ b/src/client/app/common/views/widgets/hashtags.chart.vue
@@ -0,0 +1,78 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+	<defs>
+		<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
+			<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
+			<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
+		</linearGradient>
+		<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+			<polygon
+				:points="polygonPoints"
+				fill="#fff"
+				fill-opacity="0.5"/>
+			<polyline
+				:points="polylinePoints"
+				fill="none"
+				stroke="#fff"
+				stroke-width="0.7"/>
+			<circle
+				:cx="headX"
+				:cy="headY"
+				r="1.2"
+				fill="#fff"/>
+		</mask>
+	</defs>
+	<rect
+		x="-2" y="-2"
+		:width="viewBoxX + 4" :height="viewBoxY + 4"
+		:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+	props: {
+		src: {
+			type: Array,
+			required: true
+		}
+	},
+	data() {
+		return {
+			viewBoxX: 50,
+			viewBoxY: 30,
+			gradientId: uuid(),
+			maskId: uuid(),
+			polylinePoints: '',
+			polygonPoints: '',
+			headX: null,
+			headY: null
+		};
+	},
+	watch: {
+		src() {
+			this.draw();
+		}
+	},
+	created() {
+		this.draw();
+	},
+	methods: {
+		draw() {
+			const stats = this.src.slice().reverse();
+			const peak = Math.max.apply(null, stats) || 1;
+
+			const polylinePoints = stats.map((x, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (x / peak)) * this.viewBoxY]);
+			this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+			this.polygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+
+			this.headX = polylinePoints[polylinePoints.length - 1][0];
+			this.headY = polylinePoints[polylinePoints.length - 1][1];
+		}
+	}
+});
+</script>
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
index 0ac62af70..c4647ee0f 100644
--- a/src/client/app/common/views/widgets/hashtags.vue
+++ b/src/client/app/common/views/widgets/hashtags.vue
@@ -6,7 +6,12 @@
 		<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 			<div v-else>
-				<router-link v-for="stat in stats" :key="stat.tag" :to="`/tags/${ stat.tag }`">{{ stat.tag }}</router-link>
+				<div v-for="stat in stats" :key="stat.tag">
+					<div class="tag">
+						<router-link :to="`/tags/${ stat.tag }`">#{{ stat.tag }}</router-link>
+					</div>
+					<x-chart class="chart" :src="stat.chart"/>
+				</div>
 			</div>
 		</div>
 	</mk-widget-container>
@@ -15,12 +20,17 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import XChart from './hashtags.chart.vue';
+
 export default define({
 	name: 'hashtags',
 	props: () => ({
 		compact: false
 	})
 }).extend({
+	components: {
+		XChart
+	},
 	data() {
 		return {
 			stats: [],
@@ -52,21 +62,8 @@ export default define({
 
 <style lang="stylus" scoped>
 root(isDark)
-	.mkw-rss--body
-		.feed
-			padding 12px 16px
-			font-size 0.9em
-
-			> a
-				display block
-				padding 4px 0
-				color isDark ? #9aa4b3 : #666
-				border-bottom dashed 1px isDark ? #1c2023 : #eee
-
-				&:last-child
-					border-bottom none
-
-		.fetching
+	.mkw-hashtags--body
+		> .fetching
 			margin 0
 			padding 16px
 			text-align center
@@ -75,23 +72,29 @@ root(isDark)
 			> [data-fa]
 				margin-right 4px
 
-		&[data-mobile]
-			background isDark ? #21242f : #f3f3f3
+		> div
+			> div
+				display flex
+				align-items center
+				padding 16px
 
-			.feed
-				padding 0
+				&:not(:last-child)
+					border-bottom solid 1px #393f4f
 
-				> a
-					padding 8px 16px
-					border-bottom none
+				> .tag
+					flex 1
 
-					&:nth-child(even)
-						background isDark ? rgba(#000, 0.05) : rgba(#fff, 0.7)
+					> a
+						color #9baec8
 
-.mkw-rss[data-darkmode]
+				> .chart
+					width 50px
+					height 30px
+
+.mkw-hashtags[data-darkmode]
 	root(true)
 
-.mkw-rss:not([data-darkmode])
+.mkw-hashtags:not([data-darkmode])
 	root(false)
 
 </style>
diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts
index 3b95e1fa4..6a49fec3d 100644
--- a/src/server/api/endpoints/hashtags/trend.ts
+++ b/src/server/api/endpoints/hashtags/trend.ts
@@ -4,13 +4,10 @@ import Note from '../../../../models/note';
  * Get trends of hashtags
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// 10分
-	const interval = 1000 * 60 * 10;
-
 	const data = await Note.aggregate([{
 		$match: {
 			createdAt: {
-				$gt: new Date(Date.now() - interval)
+				$gt: new Date(Date.now() - 1000 * 60 * 60)
 			},
 			tags: {
 				$exists: true,
@@ -48,6 +45,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		}>
 	}>;
 
+	if (data.length == 0) {
+		return res([]);
+	}
+
 	const hots = data[0].tags
 		.sort((a, b) => a.count - b.count)
 		.map(tag => tag.tag)
@@ -56,6 +57,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const countPromises: Array<Promise<number[]>> = [];
 
 	for (let i = 0; i < 10; i++) {
+		// 10分
+		const interval = 1000 * 60 * 10;
+
 		countPromises.push(Promise.all(hots.map(tag => Note.count({
 			tags: tag,
 			createdAt: {