Browse Source

Show hashtag ranking on dashboard

Carsten Porth 5 years ago
parent
commit
faf5b0ff10
4 changed files with 184 additions and 99 deletions
  1. 2 1
      dashboard/README.md
  2. 67 0
      dashboard/app.js
  3. 89 98
      dashboard/index.html
  4. 26 0
      dashboard/style.css

+ 2 - 1
dashboard/README.md

@@ -6,5 +6,6 @@ This website is used to provide Twitter some insights of the private P2P network
 
 - [Bootstrap](https://getbootstrap.com) with [Bootswatch Theme Darkly](https://bootswatch.com/darkly/)
 - [D3.js](https://d3js.org)
-- [jQuery](https://jquery.com/)
+- [Vue.js](https://vuejs.org)
 - [Gun](https://gun.eco/)
+- [Underscore.js](https://underscorejs.org)

+ 67 - 0
dashboard/app.js

@@ -0,0 +1,67 @@
+const gun = Gun("https://hybrid-osn.herokuapp.com/gun");
+const gunId = "hybridOSN-beta003/hashtags";
+
+const app = new Vue({
+  el: "#app",
+  data: {
+    date: new Date().toISOString().slice(0, 10),
+    keysByDate: {},
+    done: 0,
+    total: 0,
+    showLoading: false,
+    hashtags: []
+  },
+  computed: {
+    hashtagKeys() {
+      if (this.date in this.keysByDate) {
+        return this.keysByDate[this.date];
+      } else {
+        return [];
+      }
+    },
+    hashtagRanking() {
+      const ranking = _.countBy(this.hashtags, hashtag => hashtag);
+      const sortableHelper = [];
+      for (let entry in ranking) {
+        sortableHelper.push({ hashtags: entry, counts: ranking[entry] });
+      }
+      const sortedRanking = sortableHelper.sort(
+        (a, b) => b["counts"] - a["counts"]
+      );
+      return sortedRanking;
+    }
+  },
+  async mounted() {
+    // load data and group by date
+    const data = await gun.get(gunId).then();
+    const keys = Object.keys(data).filter(val => /^[0-9]+$/.test(val));
+    this.keysByDate = _.groupBy(keys, timestamp =>
+      new Date(parseInt(timestamp)).toISOString().slice(0, 10)
+    );
+    this.loadHashtags();
+  },
+  methods: {
+    async loadHashtags() {
+      // show loading
+      this.showLoading = true;
+
+      // (re)set vars
+      this.total = this.hashtagKeys.length;
+      this.hashtags = [];
+
+      // fetch tweets
+      for (let i = 0; i < this.total; i++) {
+        this.done = i + 1;
+
+        let data = await gun
+          .get(gunId)
+          .get(this.hashtagKeys[i])
+          .then();
+        this.hashtags.push(data["hashtags"]);
+      }
+
+      // finish loading
+      this.showLoading = false;
+    }
+  }
+});

+ 89 - 98
dashboard/index.html

@@ -1,115 +1,106 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <title>HybridOSN - Dashboard - Trending Topics</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
 
-    <link
-      rel="stylesheet"
-      href="https://bootswatch.com/4/darkly/bootstrap.min.css"
-    />
-  </head>
-  <body>
-    <nav class="navbar navbar-expand-md navbar-light bg-light">
-      <a href="#" class="navbar-brand">HybridOSN - Dashboard</a>
+<head>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <title>HybridOSN - Dashboard - Trending Topics</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <meta name="author" content="Carsten Porth <carsten.porth@stud.tu-darmstadt.de" />
 
-      <button
-        class="navbar-toggler"
-        type="button"
-        data-toggle="collapse"
-        data-target="#navbarSupportedContent"
-        aria-controls="navbarSupportedContent"
-        aria-expanded="false"
-        aria-label="Toggle navigation"
-      >
-        <span class="navbar-toggler-icon"></span>
-      </button>
+  <link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css" />
+  <link rel="stylesheet" href="style.css">
+</head>
 
-      <div class="collapse navbar-collapse" id="navbarSupportedContent">
-        <ul class="navbar-nav ml-auto">
-          <li class="nav-item">
-            <span
-              class="nav-link"
-              data-toggle="modal"
-              data-target="#aboutModal"
-            >
-              About
-            </span>
-          </li>
-        </ul>
-      </div>
-    </nav>
+<body>
+  <nav class="navbar navbar-expand-md navbar-light bg-light">
+    <a href="#" class="navbar-brand">HybridOSN - Dashboard</a>
+
+    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+      <span class="navbar-toggler-icon"></span>
+    </button>
 
-    <!-- Modal -->
-    <div
-      class="modal fade"
-      id="aboutModal"
-      tabindex="-1"
-      role="dialog"
-      aria-hidden="true"
-    >
-      <div class="modal-dialog" role="document">
-        <div class="modal-content">
-          <div class="modal-header">
-            <h5 class="modal-title" id="exampleModalLabel">About HybridOSN</h5>
-            <button
-              type="button"
-              class="close"
-              data-dismiss="modal"
-              aria-label="Close"
-            >
-              <span aria-hidden="true">&times;</span>
-            </button>
-          </div>
-          <div class="modal-body"><p>tbd;</p></div>
+    <div class="collapse navbar-collapse" id="navbarSupportedContent">
+      <ul class="navbar-nav ml-auto">
+        <li class="nav-item">
+          <span class="nav-link" data-toggle="modal" data-target="#aboutModal" style="cursor: pointer">
+            About
+          </span>
+        </li>
+      </ul>
+    </div>
+  </nav>
+
+  <!-- about modal -->
+  <div class="modal fade" id="aboutModal" tabindex="-1" role="dialog" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+      <div class="modal-content">
+        <div class="modal-header">
+          <h5 class="modal-title" id="exampleModalLabel">About HybridOSN</h5>
+          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+            <span aria-hidden="true">&times;</span>
+          </button>
+        </div>
+        <div class="modal-body">
+          <p>tbd;</p>
         </div>
       </div>
     </div>
+  </div>
 
-    <div class="container mt-3">
-      <h1>Trending Topics</h1>
-      <div class="row">
-        <div class="col-md-8">
-          <!--
+  <div class="container mt-3" id="app">
+    <h1 class="mb-4">
+      Trending Topics on <input type="date" v-model="date" v-on:change="loadHashtags">
+    </h1>
+    <div class="row">
+      <div class="col-md-8">
+        <!--
             D3.js: Bubble for each hashtag, size is relative to popularity
           -->
+      </div>
+      <div class="col-md-4">
+        <!-- hashtag ranking -->
+        <table class="table table-striped table-hover" v-if="hashtagRanking.length">
+          <thead>
+            <tr>
+              <th scope="col">Rank</th>
+              <th scope="col">Hashtag</th>
+              <th scope="col">Counts</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="(hashtagRank, index) in hashtagRanking">
+              <th scope="row" class="text-center">{{ index + 1 }}.</th>
+              <td>{{ hashtagRank.hashtags }}</td>
+              <td class="text-right">{{ hashtagRank.counts }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+
+    <!-- loading overlay -->
+    <div class="loading-overlay" v-if="showLoading">
+      <div class="loading-container">
+        <div class="spinner-border" style="width: 4rem; height: 4rem;" role="status">
+          <span class="sr-only">Loading...</span>
         </div>
-        <div class="col-md-4">
-          <table class="table table-striped table-hover">
-            <thead>
-              <tr>
-                <th scope="col">Rank</th>
-                <th scope="col">Hashtag</th>
-                <th scope="col">Counts</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr>
-                <th scope="row">1</th>
-                <td>private</td>
-                <td>74</td>
-              </tr>
-              <tr>
-                <th scope="row">2</th>
-                <td>P2P</td>
-                <td>38</td>
-              </tr>
-              <tr>
-                <th scope="row">3</th>
-                <td>Darmstadt</td>
-                <td>11</td>
-              </tr>
-            </tbody>
-          </table>
+        <div class="loading-progress mt-3">
+          Loading {{ done }} of {{ total }}
         </div>
       </div>
     </div>
+  </div>
+
+  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.2.1/js/bootstrap.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/gun/lib/then.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.js"></script>
+  <script src="app.js"></script>
+</body>
 
-    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
-  </body>
-</html>
+</html>

+ 26 - 0
dashboard/style.css

@@ -0,0 +1,26 @@
+input[type="date"] {
+    background: transparent;
+    border-top: none;
+    border-left: none;
+    border-right: none;
+    border-bottom: 2px solid #666;
+    color: #efefef;
+    text-align: center;
+}
+
+.loading-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+    background-color: rgba(50, 50, 50, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.loading-container {
+    text-align: center;
+    color: #aaa;
+}