Creating a chart from sentiments of user tweets using chart.js

Hi, I’m new to Django. I’m currently doing sentiment analysis on real-time user tweets via Twitter API. I have managed to do the analysis and display the sentiments. Now, I want to visualize the sentiments using charts in my Django app (perhaps bar chart or pie chart) but I’m not sure how.

I was thinking of using Chart.js to make it responsive but most of the examples are using static data so I wasn’t successful in integrating my data where I extracted from Twitter API with chart.js.

This is a screenshot of my web page. The table was the extracted tweets with their corresponding sentiments. The bar chart however is just dummy data.

This is my views.py

from django.http.response import JsonResponse
from django.shortcuts import render, redirect, HttpResponse
from .forms import Sentiment_Typed_Tweet_analyse_form
from .sentiment_analysis_code import sentiment_analysis_code
from .forms import Sentiment_Imported_Tweet_analyse_form
from .tweepy_sentiment import Import_tweet_sentiment
from django.contrib.auth.decorators import login_required
from django.contrib import messages


import plotly.express as px


@login_required(login_url='user:login')
def sentiment_analysis(request):
    return render(request, 'home/sentiment.html')

@login_required(login_url='user:login')
def sentiment_analysis_type(request):
    if request.method == 'POST':
        form = Sentiment_Typed_Tweet_analyse_form(request.POST)
        analyse = sentiment_analysis_code()
        if form.is_valid():
            tweet = form.cleaned_data['sentiment_typed_tweet']
            sentiment = analyse.get_tweet_sentiment(tweet)
            args = {'tweet':tweet, 'sentiment':sentiment}
            return render(request, 'home/sentiment_type_result.html', args)

    else:
        form = Sentiment_Typed_Tweet_analyse_form()
        return render(request, 'home/sentiment_type.html')


@login_required(login_url='user:login')
def sentiment_analysis_import(request):
    if request.method == 'POST':
        form = Sentiment_Imported_Tweet_analyse_form(request.POST)
        tweet_text = Import_tweet_sentiment()
        analyse = sentiment_analysis_code()

        if form.is_valid():
            handle = form.cleaned_data['sentiment_imported_tweet']
            # messages.info(request, 'It might take a while to load the data.')

            if handle[0]!='#':
                list_of_tweets = tweet_text.get_hashtag(handle)
                list_of_tweets_and_sentiments = []
                for i in list_of_tweets:
                    list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))
                args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle}
                return render(request, 'home/sentiment_import_result_hashtag.html', args)
            
            if handle[0]=='#':
                list_of_tweets = tweet_text.get_hashtag(handle)
                list_of_tweets_and_sentiments = []
                for i in list_of_tweets:
                    list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))
                args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle}
                return render(request, 'home/sentiment_import_result_hashtag.html', args)
            
    else:
        form = Sentiment_Imported_Tweet_analyse_form()
        return render(request, 'home/sentiment_import.html')


# def get_data(request, *args, **kwargs):
def get_data(request, *args, **kwargs):
    sentiment_analysis_import(args)
    data = {
        args
    }
    return JsonResponse('home/sentiment_import_result_hashtag.html', data)

My urls.py

from django.urls import path
from django.conf.urls import url
from . import views

app_name = 'sentiment'

urlpatterns = [
    url(r'^$', views.sentiment_analysis, name="sentiment_anaylsis"),
    url(r'^type/$', views.sentiment_analysis_type, name="sentiment_analysis_type"),
    url(r'^import/$', views.sentiment_analysis_import, name="sentiment_analysis_import"),    
]

my sentiment_import_result_hashtag.html

<!DOCTYPE html>
<html lang="en">
{% load static %}
  <head>
    <title>Sentymeter: Import Tweets</title>
   <!-- Required meta tags -->
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   <!-- Bootstrap CSS -->
   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <!--Chart js-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js" integrity="sha256-Uv9BNBucvCPipKQ2NS9wYpJmi8DTOEfTA/nH2aoJALw=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.css" integrity="sha256-aa0xaJgmK/X74WM224KMQeNQC2xYKwlAt08oZqjeF0E=" crossorigin="anonymous" />
    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
   <title>Import Tweets</title>

   <style>
     .tbl-header {
      border: 5px solid;
    	background-color: #fff;
    }
     table {
  font-family: arial, sans-serif;
  border-collapse: collapse;
  width: 100%;
}

td, th {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

th{
  color: white;
  background: black;
}

tr:nth-child(even) {
  background-color: #e4e9e4;
}
tr:nth-child(odd) {
  background-color: #c2c2c2;
}

body,
		html {
			margin: 0;
			padding: 0;
			height: 100%;
			background: #f8f2ce !important;
		}
    h7.white-text {
         color: rgb(255, 255, 255);
        }
    
</style>
   </style>
   </head>
   <body>

  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <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>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav mr-auto">
            <li class="nav-item">
              <a class="nav-link" href="/">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/sentiment/type">Input Text</a>
        </li>
              <li class="nav-item active">
                  <a class="nav-link" href="/sentiment/import">Import Tweets<span class="sr-only">(current)</span></a>
              </li>
              <li class="nav-item">
                  <a class="nav-link" href="/feedback">Feedback</a>
              </li>
          </ul>
      </div>
      <span class="hello-msg"><strong>Hello, {{request.user}}</strong></span>&nbsp;&nbsp;&nbsp;
      <button class="btn btn-danger navbar-btn" ><a href="{% url 'user:logout' %}"><h7 class="white-text" color=white;>Logout</h7></a></button>
  </nav>


      <div class="container">
        <div class="row align-items-center ftco-vh-100">
          <div class="col-md-9">
            <h1 class="ftco-heading mb-3" >Import Tweets Result</h1>
            <h2 class="h5 ftco-subheading mb-5" ><strong>Keyword</strong> - {{ handle }}</h2>
      </div><div></div>   
      
      <div class = "container">
      
      <h1>test</h1>
    </div>
    <div class="col-sm-4">
    <canvas id="myChart" width="90" height="90"></canvas>
<script>
const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['Positive', 'Neutral', 'Negative'] ,
        datasets: [{
            label: 'Sentiment Counts',
            data: [12, 19, 3],
            backgroundColor: [
                'green',
                'orange',
                'red',
            ],
        }]
    },
    options: {
        scales: {
            y: {
                beginAtZero: true
            }
        }
    }
});
</script></div>
 

<div class="tbl-header" >
    <table>
        <tr>
          <th >Tweet</th>
          <th>Sentiment</th>
          <th>Emotag</th>
        </tr>
      <tbody>
      	{% for i,j in list_of_tweets_and_sentiments %}
        <tr>
          <td>{{i}}</td>
          <td>{{j}}</td>
          {% ifequal j 'Negative' %}
          <td><img src="{% static 'icons/negative.png' %}"></td>
          {% endifequal %}
          {% ifequal j 'Positive' %}
          <td><img src="{% static 'icons/positive.png' %}"></td>
          {% endifequal %}
          {% ifequal j 'Neutral' %}
          <td><img src="{% static 'icons/neutral.png' %}"></td>
          {% endifequal %}
        </tr>
        {% endfor %}
      </tbody>
    </table>
  </div>


<h1>test</h1>


<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  </body>
</html>

Can anyone help me on how to link my sentiment tweets with the chart? I am so confused right now… :frowning:

Please let me know if you need other files/code.

Hi,

I guess your current issue is providing dynamic data to the Charts.js library. Perhaps you could look at the static sample data and try to output your dynamic ones in the same format from Django?

You can look at the json_script filter. This will safelly output your data to be used with JS.

Do I need to create a certain method in the views.py or do I just need to update the JSON script in the HTML? I tried but I’m still not getting it.

Based on the screenshot above, you already have the page to display the chart - which means corresponding view in the views.py.

What I think you need to do:

  • Extend your context and add your dynamic data
  • Render these data in your template with json_script
  • Tell Charts.js library to use this data instead of the dummy one

one way to do this
in veiws.py you can add loop in def sentiment_analysis_import(request):

if handle[0]==’#’:

            list_of_tweets = tweet_text.get_hashtag(handle)

            list_of_tweets_and_sentiments = []

            for i in list_of_tweets:

                list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))

for i,j in list_of_tweets_and_sentiments:

                if j =="Negative":

                    neg = neg + 1

                elif j == "Positive":
                    pos = pos+1
                else :
                    neu = neu +1     
            args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle,'neg':neg,'pos':pos,"neu":neu}

after this u can use this data in template

Hi, thanks for taking your time to reply.

I already tried the count method as u mentioned in the views.py but I’m not sure how to call it in the template? Do I need to convert it into JSON or just call it just like that? but Im still not sure if im doing it right… it still doesn’t display any chart.

Im quite confused on how to call the data in the page and how to put it in the chart data script.

The comments are the bunch of stuff I tried but also failed… Can show me how to do it?

  
  <!-- {{ list_of_tweets_and_sentiments|json_script:"list_of_tweets_and_sentiments" }}
  {{ pos|json_script:"pos-data" }}
  {{ neu|json_script:"neu-data" }}
  {{ neg|json_script:"neg-data" }} -->

  <!-- <script id="pos-data" type="application/json">{'pos':pos}</script>
  <script id="neu-data" type="application/json">{'neu':neu}</script>
  <script id="neg-data" type="application/json">{'neg':neg}</script> -->


<canvas id="list_of_tweets_and_sentiments" width="90" height="90"></canvas>
<script id="list_of_tweets_and_sentiments" type="application/json">{{pos}},{{neu}},{{neg}}</script>

<script>
  
//  const ctx = JSON.parse(document.getElementById('pos-data','neu-data','neg-data')).getContext('2d');
//  const ctx = JSON.parse(document.getElementById('pos-data','neu-data','neg-data')).getContext('2d');

const ctx = document.getElementById('list_of_tweets_and_sentiments').getContext('2d');
const list_of_tweets_and_sentiments = new Chart(ctx, {
  // const myChart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['Positive', 'Neutral', 'Negative'] ,
        datasets: [{
            label: 'Sentiment Counts',
            data: pos,neu,neg,

            backgroundColor: [
                'green',
                'orange',
                'red',
            ],
        }]
    },
    options: {
        scales: {
            y: {
                beginAtZero: true
            }
        }
    }
});
</script></div>

Looks like you are on the right track. Did you check the Console in your browser for any errors Charts.js may report?

it doesn’t show any errors but it doesn’t load the chart. So I dont know if im even doing it right… I cant seem to pinpoint what might be the problem.

in my views.py

def sentiment_analysis_import(request):
    if request.method == 'POST':
        form = Sentiment_Imported_Tweet_analyse_form(request.POST)
        tweet_text = Import_tweet_sentiment()
        analyse = sentiment_analysis_code()

        if form.is_valid():
            handle = form.cleaned_data['sentiment_imported_tweet']
            # messages.info(request, 'It might take a while to load the data.')

            if handle[0]!='#':
                list_of_tweets = tweet_text.get_hashtag(handle)
                list_of_tweets_and_sentiments = []
                for i in list_of_tweets:
                    list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))
                args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle}
                return render(request, 'home/sentiment_import_result_hashtag.html', args)
            
            if handle[0]=='#':
                list_of_tweets = tweet_text.get_hashtag(handle)
                list_of_tweets_and_sentiments = []
                for i in list_of_tweets:
                    list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))
                
                for i,j in list_of_tweets_and_sentiments:
                     if j =="Negative":
                          neg = neg + 1
                     elif j == "Positive":
                         pos = pos+1
                     else :
                         neu = neu +1
                
                args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle,'neg':neg,'pos':pos,'neu':neu}
                return render(request, 'home/sentiment_import_result_hashtag.html', args)
                # return JsonResponse(request, 'home/sentiment_import_result_hashtag.html', args)
            
    else:
        form = Sentiment_Imported_Tweet_analyse_form()
        return render(request, 'home/sentiment_import.html')

Im not sure which one I’m supposed to used for this the return part:

return render(request, 'home/sentiment_import_result_hashtag.html', args)            

or

return JsonResponse(request, 'home/sentiment_import_result_hashtag.html', args)

this is my html

 
  <canvas id="myChart" width="90" height="90"></canvas>
  <script>
  const ctx = document.getElementById('myChart').getContext('2d');
  const myChart = new Chart(ctx, {
      type: 'bar',
      data: {
          // labels: ['Positive','Neutral','Negative'] ,
          labels: ['{{pos}}','{{neu}}','{{neg}}', ] ,

          datasets: [{
              label: 'Counts',
              // data: [{'pos':pos},{'neu':neu},{'neg':neg}],
              // data: [{pos},{neu},{neg}],
              // data: [pos,neu,neg],
              data: [{{pos}} ,{{neu}} ,{{neg}} ,],
  
              backgroundColor: [
                  'green',
                  'orange',
                  'red',
              ],
          }]
      },
      options: {
          scales: {
              y: {
                  beginAtZero: true
              }
          }
      }
  });
  </script>

I tried all of these but it doesn’t show any chart or the chart axis:

// data: [{'pos':pos},{'neu':neu},{'neg':neg}],
              // data: [{pos},{neu},{neg}],
              // data: [pos,neu,neg],

I watched a video tutorial where the person uses dynamic data from database, the editors have errors with this method but apparently it works for him. This is a screenshot of his code:

and this is his web page:

So tried his method but I still dont really understand. It has errors but now the chart axis appeared in my web page without the data. (Previously the axis didn’t even appear)

 data: {
          labels: ['{{pos}}','{{neu}}','{{neg}}', ] ,

          datasets: [{
              label: 'Counts',
              data: [{{pos}} ,{{neu}} ,{{neg}} ,],

this is my web page:

This is the web console:

I get errors like this in my editor for the data part I did:

“Property assignment expected.”

Thoughts?

The errors in VS Code are likely because it is confused by the Django template tags… Javascript won’t see the {{ pos }} and other variables, because they will get rendered before JS is executed.

Can you check the page source in the browser what the resulting JS looks like? Mainly the part with const myChart?

This is how it looks like.

I also tried to display the data outside of the chart script but it also didn’t work.

<div class="container">
        <div class="row align-items-center ftco-vh-100">
          <div class="col-md-9">
            <h1 class="ftco-heading mb-3" >Import Tweets Result</h1>
            <h2 class="h5 ftco-subheading mb-5" ><strong>Keyword</strong> - {{ handle }}</h2>
      </div><div></div>   
      
      <div class = "container">
      <h1>neutral counts: {{ neu }}</h1>
      <h1>positive counts: {{ pos }}</h1>
      <h1>negative counts: {{ neg }}</h1>
      
    </div>

web page:

page source:

if you look at here, at the commented line, the list_of_tweets_and_sentiments json detects the data. but the other json didn’t .

or could it be that the code to count the neu, pos, neg didn’t work?

I think we found the issue. Your template variables neu, pos and neg aren’t defined in your context and are rendered as blank. That is likely the reason chart doesn’t display anything - because it has no data.

My guess is that this condition if handle[0]=='#': is never getting executed.

You most likely always end up at this:

args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle}
return render(request, 'home/sentiment_import_result_hashtag.html', args)            

And this is causing the variables to be empty.

Ah, I see. Because I didn’t add the sentiment count for the if handle =!'#'

So I added it.

            if handle[0]!='#':
                list_of_tweets = tweet_text.get_hashtag(handle)
                list_of_tweets_and_sentiments = []
                for i in list_of_tweets:
                    list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))
                
                for i,j in list_of_tweets_and_sentiments:
                        if j =="Negative":
                            neg = neg + 1
                        elif j == "Positive":
                            pos = pos+1
                        else :
                            neu = neu +1

                args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle, 'neg':neg,'pos':pos,"neu":neu}
                return render(request, 'home/sentiment_import_result_hashtag.html', args)
            
            if handle[0]=='#':
                list_of_tweets = tweet_text.get_hashtag(handle)
                list_of_tweets_and_sentiments = []
                for i in list_of_tweets:
                    list_of_tweets_and_sentiments.append((i,analyse.get_tweet_sentiment(i)))

                for i,j in list_of_tweets_and_sentiments:
                     if j =="Negative":
                          neg = neg + 1
                     elif j == "Positive":
                         pos = pos+1
                     else :
                         neu = neu +1
                
                args = {'list_of_tweets_and_sentiments':list_of_tweets_and_sentiments, 'handle':handle,'neg':neg,'pos':pos,"neu":neu}
                return render(request, 'home/sentiment_import_result_hashtag.html', args)

but now I get this error in the web page:

I think the error is in this one: neu = neu +1