├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── js ├── canvasjs.min.js └── sentiment-analysis.js ├── php ├── TwitterAPIExchange.php └── queryTwitter.php └── style └── sentiment-analysis.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: bensonruan 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | php/cacert.pem 3 | php/phpInfo.php 4 | node_modules/* 5 | twitter-sentiment-analysis.gif 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Benson Ruan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentiment-Analysis 2 | Twitter Sentiment Analysis with Tensorflow.js 3 | 4 | Connect to Twitter API, gather tweets by hashtag, compute the sentiment of each tweet, and build a real-time dashboard to show the result. 5 | 6 | ## Live Demo 7 | **[https://bensonruan.com/twitter-sentiment-analysis-with-tensorflowjs](https://bensonruan.com/twitter-sentiment-analysis-with-tensorflowjs)** 8 | 9 | ![sentiment](https://bensonruan.com/wp-content/uploads/2019/10/twitter-sentiment-analysis.gif) 10 | 11 | 12 | ## Installing 13 | 1. Clone this repository to your local computer 14 | ``` bash 15 | git https://github.com/bensonruan/Sentiment-Analysis.git 16 | ``` 17 | 18 | 2. On Twitter developer platform https://developer.twitter.com/ 19 | * Register a Twitter dev account 20 | * Create a Twitter App 21 | * Get the Consumer API keys and Access tokens 22 | * Replace your API keys in queryTwitter.php 23 | 24 | 25 | 3. Config your path to the queryTwitter.php inside sentiment-analysis.js and sentiment-analysis-bundle.js 26 | ``` bash 27 | queryTwitter: window.location.protocol + '//'+ window.location.hostname + '/js/sentiment/queryTwitter.php?q=' 28 | ``` 29 | 30 | 4. Point your localhost to the cloned root directory. Browse to http://localhost/index.html 31 | 32 | 33 | ## Note 34 | If you are on Windows, you would need to install PHP via Web Platform Installer 35 | 36 | ## Library 37 | * [twitter-api-php](https://github.com/J7mbo/twitter-api-php) - PHP Wrapper for Twitter API v1.1 calls 38 | * [jquery](https://code.jquery.com/jquery-3.3.1.min.js) - JQuery 39 | * [tensorflow.js sentiment](https://github.com/tensorflow/tfjs-examples/tree/master/sentiment) - Perform text sentiment analysis on text using the Layers API of TensorFlow.js 40 | * [canvasjs](https://canvasjs.com/jquery-charts/) - JQuery chart library 41 | 42 | ## Support me 43 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/W7W6METMY) 44 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Twitter Sentiment Analysis 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |

Twitter Sentiment Analysis

20 |
21 |
22 |
23 |
24 | Type in any hashtag or keyword and press enter to visualize Tweet Sentiment. 25 |
26 |
27 |
28 |
29 | # 30 | 31 | 34 |
35 |
36 |
37 |
38 |
39 | Loading... 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /js/sentiment-analysis.js: -------------------------------------------------------------------------------- 1 | const HOSTED_URLS = { 2 | queryTwitter: window.location.protocol + '//'+ window.location.hostname + '/js/sentiment/queryTwitter.php?q=', 3 | model: 'https://storage.googleapis.com/tfjs-models/tfjs/sentiment_cnn_v1/model.json', 4 | metadata: 'https://storage.googleapis.com/tfjs-models/tfjs/sentiment_cnn_v1/metadata.json' 5 | }; 6 | const LOCAL_URLS = { 7 | queryTwitter: 'php/queryTwitter.php?q=', 8 | model: 'https://storage.googleapis.com/tfjs-models/tfjs/sentiment_cnn_v1/model.json', 9 | metadata: 'https://storage.googleapis.com/tfjs-models/tfjs/sentiment_cnn_v1/metadata.json' 10 | }; 11 | const SentimentThreshold = { 12 | Positive: 0.66, 13 | Neutral: 0.33, 14 | Negative: 0 15 | } 16 | const PAD_INDEX = 0; 17 | const OOV_INDEX = 2; 18 | 19 | let urls, model, metadata; 20 | 21 | $("#tag-input").on('keyup', function (e) { 22 | if (e.keyCode === 13) { 23 | twitterSentiment(); 24 | } 25 | }); 26 | 27 | $(".btn-search").click(function () { 28 | twitterSentiment(); 29 | }); 30 | 31 | function init(){ 32 | if(window.location.hostname == 'localhost'){ 33 | urls = LOCAL_URLS; 34 | }else { 35 | urls = HOSTED_URLS; 36 | } 37 | } 38 | 39 | async function setupSentimentModel(){ 40 | if(typeof model === 'undefined'){ 41 | model = await loadModel(urls.model); 42 | } 43 | if(typeof metadata === 'undefined'){ 44 | metadata = await loadMetadata(urls.metadata); 45 | } 46 | } 47 | 48 | function twitterSentiment(){ 49 | $('#tweet-list').addClass('d-none'); 50 | $('#positive').empty(); 51 | $('#neutral').empty(); 52 | $('#negative').empty(); 53 | $('#chartContainer').empty(); 54 | $('.spinner-border').removeClass('d-none'); 55 | 56 | getTwitterHashTagData($("#tag-input").val(), processTwitterData); 57 | } 58 | 59 | function processTwitterData(tweets){ 60 | setupSentimentModel().then( 61 | result => { 62 | const twitterData = []; 63 | $.each(tweets, function( index, tweet ) { 64 | const tweet_text = tweet.full_text.replace(/(?:https?|ftp):\/\/[\n\S]+/g, ''); 65 | const sentiment_score = getSentimentScore(tweet_text); 66 | let tweet_sentiment = ''; 67 | if(sentiment_score > SentimentThreshold.Positive){ 68 | tweet_sentiment = 'positive' 69 | }else if(sentiment_score > SentimentThreshold.Neutral){ 70 | tweet_sentiment = 'neutral' 71 | }else if(sentiment_score >= SentimentThreshold.Negative){ 72 | tweet_sentiment = 'negative' 73 | } 74 | twitterData.push({ 75 | sentiment: tweet_sentiment, 76 | score: sentiment_score.toFixed(4), 77 | tweet: tweet_text 78 | }); 79 | }); 80 | console.log(twitterData); 81 | $('.spinner-border').addClass('d-none'); 82 | displayTweets(twitterData.filter(t => t.sentiment == 'positive'), 'positive'); 83 | displayTweets(twitterData.filter(t => t.sentiment == 'neutral'), 'neutral'); 84 | displayTweets(twitterData.filter(t => t.sentiment == 'negative'), 'negative'); 85 | $('#tweet-list').removeClass('d-none'); 86 | displayPieChart(twitterData); 87 | } 88 | ) 89 | } 90 | 91 | async function loadModel(url) { 92 | try { 93 | const model = await tf.loadLayersModel(url); 94 | return model; 95 | } catch (err) { 96 | console.log(err); 97 | } 98 | } 99 | 100 | async function loadMetadata(url) { 101 | try { 102 | const metadataJson = await fetch(url); 103 | const metadata = await metadataJson.json(); 104 | return metadata; 105 | } catch (err) { 106 | console.log(err); 107 | } 108 | } 109 | 110 | function padSequences(sequences, maxLen, padding = 'pre', truncating = 'pre', value = PAD_INDEX) { 111 | return sequences.map(seq => { 112 | if (seq.length > maxLen) { 113 | if (truncating === 'pre') { 114 | seq.splice(0, seq.length - maxLen); 115 | } else { 116 | seq.splice(maxLen, seq.length - maxLen); 117 | } 118 | } 119 | 120 | if (seq.length < maxLen) { 121 | const pad = []; 122 | for (let i = 0; i < maxLen - seq.length; ++i) { 123 | pad.push(value); 124 | } 125 | if (padding === 'pre') { 126 | seq = pad.concat(seq); 127 | } else { 128 | seq = seq.concat(pad); 129 | } 130 | } 131 | 132 | return seq; 133 | }); 134 | } 135 | 136 | function getSentimentScore(text) { 137 | const inputText = text.trim().toLowerCase().replace(/(\.|\,|\!)/g, '').split(' '); 138 | // Convert the words to a sequence of word indices. 139 | const sequence = inputText.map(word => { 140 | let wordIndex = metadata.word_index[word] + metadata.index_from; 141 | if (wordIndex > metadata.vocabulary_size) { 142 | wordIndex = OOV_INDEX; 143 | } 144 | return wordIndex; 145 | }); 146 | // Perform truncation and padding. 147 | const paddedSequence = padSequences([sequence], metadata.max_len); 148 | const input = tf.tensor2d(paddedSequence, [1, metadata.max_len]); 149 | 150 | const predictOut = model.predict(input); 151 | const score = predictOut.dataSync()[0]; 152 | predictOut.dispose(); 153 | 154 | return score; 155 | } 156 | 157 | function getTwitterHashTagData(query, callback) { 158 | $.getJSON( urls.queryTwitter + query, function(result) { 159 | console.log(result); 160 | if(result !== null && result.statuses !== null){ 161 | callback(result.statuses); 162 | } 163 | }); 164 | } 165 | 166 | function displayTweets(twitterData, sentiment){ 167 | var tbl = document.createElement('table'); 168 | var tr = tbl.insertRow(); 169 | for( var j in twitterData[0] ) { 170 | if(j !=='sentiment'){ 171 | var td = tr.insertCell(); 172 | td.appendChild(document.createTextNode(j)); 173 | } 174 | } 175 | 176 | for( var i = 0; i < twitterData.length; i++) { 177 | var tr = tbl.insertRow(); 178 | for( var j in twitterData[i] ) { 179 | if(j !=='sentiment'){ 180 | var td = tr.insertCell(); 181 | var text = twitterData[i][j]; 182 | td.appendChild(document.createTextNode(text)); 183 | } 184 | } 185 | } 186 | tbl.setAttribute('class', 'tweet-table') 187 | $('#'+sentiment).append(tbl); 188 | $('#'+sentiment+'-counter').html('('+ twitterData.length +')'); 189 | } 190 | 191 | function displayPieChart(twitterData){ 192 | var sentimentsCounter = {"Negative": 0, "Neutral": 0, "Positive": 0}; 193 | for( var i = 0; i < twitterData.length; i++) { 194 | switch(twitterData[i].sentiment) { 195 | case 'positive': 196 | sentimentsCounter["Positive"] += 1; 197 | break; 198 | case 'negative': 199 | sentimentsCounter["Negative"] += 1; 200 | break; 201 | case 'neutral': 202 | sentimentsCounter["Neutral"] += 1; 203 | break; 204 | } 205 | } 206 | 207 | var chart = new CanvasJS.Chart("chartContainer", { 208 | theme: "light2", 209 | exportEnabled: true, 210 | animationEnabled: true, 211 | data: [{ 212 | type: "pie", 213 | startAngle: 25, 214 | toolTipContent: "{label}: {y}%", 215 | showInLegend: "true", 216 | legendText: "{label}", 217 | indexLabelFontSize: 16, 218 | indexLabel: "{label} - {y}%", 219 | dataPoints: [ 220 | { y: (sentimentsCounter["Positive"] * 100.00/twitterData.length).toFixed(2), label: "Positive" }, 221 | { y: (sentimentsCounter["Neutral"] * 100.00/twitterData.length).toFixed(2), label: "Neutral" }, 222 | { y: (sentimentsCounter["Negative"] * 100.00/twitterData.length).toFixed(2), label: "Negative" }, 223 | ] 224 | }] 225 | }); 226 | chart.render(); 227 | } 228 | 229 | init(); -------------------------------------------------------------------------------- /php/TwitterAPIExchange.php: -------------------------------------------------------------------------------- 1 | 11 | * @license MIT License 12 | * @version 1.0.4 13 | * @link http://github.com/j7mbo/twitter-api-php 14 | */ 15 | class TwitterAPIExchange 16 | { 17 | /** 18 | * @var string 19 | */ 20 | private $oauth_access_token; 21 | 22 | /** 23 | * @var string 24 | */ 25 | private $oauth_access_token_secret; 26 | 27 | /** 28 | * @var string 29 | */ 30 | private $consumer_key; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private $consumer_secret; 36 | 37 | /** 38 | * @var array 39 | */ 40 | private $postfields; 41 | 42 | /** 43 | * @var string 44 | */ 45 | private $getfield; 46 | 47 | /** 48 | * @var mixed 49 | */ 50 | protected $oauth; 51 | 52 | /** 53 | * @var string 54 | */ 55 | public $url; 56 | 57 | /** 58 | * @var string 59 | */ 60 | public $requestMethod; 61 | 62 | /** 63 | * The HTTP status code from the previous request 64 | * 65 | * @var int 66 | */ 67 | protected $httpStatusCode; 68 | 69 | /** 70 | * Create the API access object. Requires an array of settings:: 71 | * oauth access token, oauth access token secret, consumer key, consumer secret 72 | * These are all available by creating your own application on dev.twitter.com 73 | * Requires the cURL library 74 | * 75 | * @throws \RuntimeException When cURL isn't loaded 76 | * @throws \InvalidArgumentException When incomplete settings parameters are provided 77 | * 78 | * @param array $settings 79 | */ 80 | public function __construct(array $settings) 81 | { 82 | if (!function_exists('curl_init')) 83 | { 84 | throw new RuntimeException('TwitterAPIExchange requires cURL extension to be loaded, see: http://curl.haxx.se/docs/install.html'); 85 | } 86 | 87 | if (!isset($settings['oauth_access_token']) 88 | || !isset($settings['oauth_access_token_secret']) 89 | || !isset($settings['consumer_key']) 90 | || !isset($settings['consumer_secret'])) 91 | { 92 | throw new InvalidArgumentException('Incomplete settings passed to TwitterAPIExchange'); 93 | } 94 | 95 | $this->oauth_access_token = $settings['oauth_access_token']; 96 | $this->oauth_access_token_secret = $settings['oauth_access_token_secret']; 97 | $this->consumer_key = $settings['consumer_key']; 98 | $this->consumer_secret = $settings['consumer_secret']; 99 | } 100 | 101 | /** 102 | * Set postfields array, example: array('screen_name' => 'J7mbo') 103 | * 104 | * @param array $array Array of parameters to send to API 105 | * 106 | * @throws \Exception When you are trying to set both get and post fields 107 | * 108 | * @return TwitterAPIExchange Instance of self for method chaining 109 | */ 110 | public function setPostfields(array $array) 111 | { 112 | if (!is_null($this->getGetfield())) 113 | { 114 | throw new Exception('You can only choose get OR post fields (post fields include put).'); 115 | } 116 | 117 | if (isset($array['status']) && substr($array['status'], 0, 1) === '@') 118 | { 119 | $array['status'] = sprintf("\0%s", $array['status']); 120 | } 121 | 122 | foreach ($array as $key => &$value) 123 | { 124 | if (is_bool($value)) 125 | { 126 | $value = ($value === true) ? 'true' : 'false'; 127 | } 128 | } 129 | 130 | $this->postfields = $array; 131 | 132 | // rebuild oAuth 133 | if (isset($this->oauth['oauth_signature'])) 134 | { 135 | $this->buildOauth($this->url, $this->requestMethod); 136 | } 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Set getfield string, example: '?screen_name=J7mbo' 143 | * 144 | * @param string $string Get key and value pairs as string 145 | * 146 | * @throws \Exception 147 | * 148 | * @return \TwitterAPIExchange Instance of self for method chaining 149 | */ 150 | public function setGetfield($string) 151 | { 152 | if (!is_null($this->getPostfields())) 153 | { 154 | throw new Exception('You can only choose get OR post / post fields.'); 155 | } 156 | 157 | $getfields = preg_replace('/^\?/', '', explode('&', $string)); 158 | $params = array(); 159 | 160 | foreach ($getfields as $field) 161 | { 162 | if ($field !== '') 163 | { 164 | list($key, $value) = explode('=', $field); 165 | $params[$key] = $value; 166 | } 167 | } 168 | 169 | $this->getfield = '?' . http_build_query($params, '', '&'); 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * Get getfield string (simple getter) 176 | * 177 | * @return string $this->getfields 178 | */ 179 | public function getGetfield() 180 | { 181 | return $this->getfield; 182 | } 183 | 184 | /** 185 | * Get postfields array (simple getter) 186 | * 187 | * @return array $this->postfields 188 | */ 189 | public function getPostfields() 190 | { 191 | return $this->postfields; 192 | } 193 | 194 | /** 195 | * Build the Oauth object using params set in construct and additionals 196 | * passed to this method. For v1.1, see: https://dev.twitter.com/docs/api/1.1 197 | * 198 | * @param string $url The API url to use. Example: https://api.twitter.com/1.1/search/tweets.json 199 | * @param string $requestMethod Either POST or GET 200 | * 201 | * @throws \Exception 202 | * 203 | * @return \TwitterAPIExchange Instance of self for method chaining 204 | */ 205 | public function buildOauth($url, $requestMethod) 206 | { 207 | if (!in_array(strtolower($requestMethod), array('post', 'get', 'put', 'delete'))) 208 | { 209 | throw new Exception('Request method must be either POST, GET or PUT or DELETE'); 210 | } 211 | 212 | $consumer_key = $this->consumer_key; 213 | $consumer_secret = $this->consumer_secret; 214 | $oauth_access_token = $this->oauth_access_token; 215 | $oauth_access_token_secret = $this->oauth_access_token_secret; 216 | 217 | $oauth = array( 218 | 'oauth_consumer_key' => $consumer_key, 219 | 'oauth_nonce' => time(), 220 | 'oauth_signature_method' => 'HMAC-SHA1', 221 | 'oauth_token' => $oauth_access_token, 222 | 'oauth_timestamp' => time(), 223 | 'oauth_version' => '1.0' 224 | ); 225 | 226 | $getfield = $this->getGetfield(); 227 | 228 | if (!is_null($getfield)) 229 | { 230 | $getfields = str_replace('?', '', explode('&', $getfield)); 231 | 232 | foreach ($getfields as $g) 233 | { 234 | $split = explode('=', $g); 235 | 236 | /** In case a null is passed through **/ 237 | if (isset($split[1])) 238 | { 239 | $oauth[$split[0]] = urldecode($split[1]); 240 | } 241 | } 242 | } 243 | 244 | $postfields = $this->getPostfields(); 245 | 246 | if (!is_null($postfields)) { 247 | foreach ($postfields as $key => $value) { 248 | $oauth[$key] = $value; 249 | } 250 | } 251 | 252 | $base_info = $this->buildBaseString($url, $requestMethod, $oauth); 253 | $composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret); 254 | $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true)); 255 | $oauth['oauth_signature'] = $oauth_signature; 256 | 257 | $this->url = $url; 258 | $this->requestMethod = $requestMethod; 259 | $this->oauth = $oauth; 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * Perform the actual data retrieval from the API 266 | * 267 | * @param boolean $return If true, returns data. This is left in for backward compatibility reasons 268 | * @param array $curlOptions Additional Curl options for this request 269 | * 270 | * @throws \Exception 271 | * 272 | * @return string json If $return param is true, returns json data. 273 | */ 274 | public function performRequest($return = true, $curlOptions = array()) 275 | { 276 | if (!is_bool($return)) 277 | { 278 | throw new Exception('performRequest parameter must be true or false'); 279 | } 280 | 281 | $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); 282 | 283 | $getfield = $this->getGetfield(); 284 | $postfields = $this->getPostfields(); 285 | 286 | if (in_array(strtolower($this->requestMethod), array('put', 'delete'))) 287 | { 288 | $curlOptions[CURLOPT_CUSTOMREQUEST] = $this->requestMethod; 289 | } 290 | 291 | $options = $curlOptions + array( 292 | CURLOPT_HTTPHEADER => $header, 293 | CURLOPT_HEADER => false, 294 | CURLOPT_URL => $this->url, 295 | CURLOPT_RETURNTRANSFER => true, 296 | CURLOPT_TIMEOUT => 10, 297 | ); 298 | 299 | if (!is_null($postfields)) 300 | { 301 | $options[CURLOPT_POSTFIELDS] = http_build_query($postfields, '', '&'); 302 | } 303 | else 304 | { 305 | if ($getfield !== '') 306 | { 307 | $options[CURLOPT_URL] .= $getfield; 308 | } 309 | } 310 | 311 | $feed = curl_init(); 312 | curl_setopt_array($feed, $options); 313 | $json = curl_exec($feed); 314 | 315 | $this->httpStatusCode = curl_getinfo($feed, CURLINFO_HTTP_CODE); 316 | 317 | if (($error = curl_error($feed)) !== '') 318 | { 319 | curl_close($feed); 320 | 321 | throw new \Exception($error); 322 | } 323 | 324 | curl_close($feed); 325 | 326 | return $json; 327 | } 328 | 329 | /** 330 | * Private method to generate the base string used by cURL 331 | * 332 | * @param string $baseURI 333 | * @param string $method 334 | * @param array $params 335 | * 336 | * @return string Built base string 337 | */ 338 | private function buildBaseString($baseURI, $method, $params) 339 | { 340 | $return = array(); 341 | ksort($params); 342 | 343 | foreach($params as $key => $value) 344 | { 345 | $return[] = rawurlencode($key) . '=' . rawurlencode($value); 346 | } 347 | 348 | return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return)); 349 | } 350 | 351 | /** 352 | * Private method to generate authorization header used by cURL 353 | * 354 | * @param array $oauth Array of oauth data generated by buildOauth() 355 | * 356 | * @return string $return Header used by cURL for request 357 | */ 358 | private function buildAuthorizationHeader(array $oauth) 359 | { 360 | $return = 'Authorization: OAuth '; 361 | $values = array(); 362 | 363 | foreach($oauth as $key => $value) 364 | { 365 | if (in_array($key, array('oauth_consumer_key', 'oauth_nonce', 'oauth_signature', 366 | 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'))) { 367 | $values[] = "$key=\"" . rawurlencode($value) . "\""; 368 | } 369 | } 370 | 371 | $return .= implode(', ', $values); 372 | return $return; 373 | } 374 | 375 | /** 376 | * Helper method to perform our request 377 | * 378 | * @param string $url 379 | * @param string $method 380 | * @param string $data 381 | * @param array $curlOptions 382 | * 383 | * @throws \Exception 384 | * 385 | * @return string The json response from the server 386 | */ 387 | public function request($url, $method = 'get', $data = null, $curlOptions = array()) 388 | { 389 | if (strtolower($method) === 'get') 390 | { 391 | $this->setGetfield($data); 392 | } 393 | else 394 | { 395 | $this->setPostfields($data); 396 | } 397 | 398 | return $this->buildOauth($url, $method)->performRequest(true, $curlOptions); 399 | } 400 | 401 | /** 402 | * Get the HTTP status code for the previous request 403 | * 404 | * @return integer 405 | */ 406 | public function getHttpStatusCode() 407 | { 408 | return $this->httpStatusCode; 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /php/queryTwitter.php: -------------------------------------------------------------------------------- 1 | "YOUR_ACCESS_TOKEN", 8 | 'oauth_access_token_secret' => "YOUR_ACCESS_TOKEN_SECRET", 9 | 'consumer_key' => "YOUR_CONSUMER_KEY", 10 | 'consumer_secret' => "YOUR_CONSUMER_SECRET" 11 | ); 12 | 13 | $url = 'https://api.twitter.com/1.1/search/tweets.json'; 14 | $getfield = '?q=#'.$hashtag.' AND -filter:retweets AND -filter:replies&lang=en&count=20&tweet_mode=extended'; 15 | $requestMethod = 'GET'; 16 | 17 | $twitter = new TwitterAPIExchange($settings); 18 | $response = $twitter->setGetfield($getfield) 19 | ->buildOauth($url, $requestMethod) 20 | ->performRequest(); 21 | 22 | echo $response; 23 | ?> -------------------------------------------------------------------------------- /style/sentiment-analysis.css: -------------------------------------------------------------------------------- 1 | .info-text { 2 | font-size: 20px; 3 | margin: 20px auto 4 | } 5 | 6 | .input-field .prefix { 7 | position: absolute; 8 | width: 2rem; 9 | font-size: 3rem; 10 | transition: color .2s; 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | font-size: 24px; 18 | line-height: 1; 19 | letter-spacing: normal; 20 | text-transform: none; 21 | display: inline-block; 22 | white-space: nowrap; 23 | word-wrap: normal; 24 | direction: ltr; 25 | -webkit-font-feature-settings: 'liga'; 26 | -webkit-font-smoothing: antialiased; 27 | } 28 | 29 | #tag-input { 30 | font-size: 2em; 31 | background-color: transparent; 32 | border: none; 33 | border-bottom: 1px solid #9e9e9e; 34 | border-radius: 0; 35 | outline: none; 36 | height: 3rem; 37 | width: 66%; 38 | width: calc(80% - 3rem); 39 | margin: 0 0 20px 2rem; 40 | padding: 0; 41 | box-shadow: none; 42 | box-sizing: content-box; 43 | transition: all 0.3s; 44 | } 45 | 46 | @media screen and (min-width: 992px) { 47 | .btn-search { 48 | margin-bottom: 5px; 49 | } 50 | } 51 | 52 | 53 | @media screen and (max-width: 992px) { 54 | #tag-input { 55 | font-size: 1.3em; 56 | } 57 | } 58 | 59 | .tweet-table, 60 | .tweet-table td { 61 | border: 1px solid #dee2e6; 62 | border-top: 0px; 63 | } 64 | 65 | .tweet-table td { 66 | padding: 5px; 67 | } 68 | .tweet-table td:first-child { 69 | text-align: center; 70 | width: 20%; 71 | } 72 | 73 | .tweet-table td:nth-child(2) { 74 | text-align: left; 75 | width: 80%; 76 | } 77 | 78 | .spinner-border { 79 | width: 100px; 80 | height: 100px; 81 | margin: 30px auto; 82 | } 83 | 84 | #chartContainer{ 85 | height: 400px; 86 | margin: 15px; 87 | } 88 | 89 | #positive-tab.nav-link{ 90 | color: #6878AD; 91 | } 92 | #positive-tab.nav-link.active{ 93 | color: #fff; 94 | background-color: #6878AD; 95 | } 96 | #neutral-tab.nav-link.active{ 97 | color: #fff; 98 | background-color: #00D0A4; 99 | } 100 | #neutral-tab.nav-link{ 101 | color: #00D0A4; 102 | } 103 | #negative-tab.nav-link.active{ 104 | color: #fff; 105 | background-color: #F2736A; 106 | } 107 | #negative-tab.nav-link{ 108 | color: #F2736A; 109 | } 110 | .nav-link { 111 | padding: .5rem .8rem !important; 112 | } --------------------------------------------------------------------------------