Post

[DEV] Django & Plotly 연동

[DEV] Django & Plotly 연동

1. 배경

  • 18개의 기업 별 테그 블로그를 크롤링해서 태그 별 빈도수 시각화 & 글 모아 보여주기
  • ERD

2. 전체 태그 빈도수 시각화

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from .models import *
from django.http import JsonResponse
import plotly.express as px
import pandas as pd

def all_chart(request):
    tag_df = pd.DataFrame.from_records(Post_tag.objects.all().values('tag__tag_name').distinct())
    tag_df.drop(tag_df[tag_df['tag__tag_name'] == ''].index, inplace=True)

    tag = pd.DataFrame.from_records(Post_tag.objects.all().values('tag__tag_name'))
    tag.drop(tag[tag['tag__tag_name'] == ''].index, inplace=True)
    count = pd.DataFrame(tag.groupby('tag__tag_name').size().reset_index(name='count'))
    top_20 = count.sort_values(by='count', ascending=False).head(20)

    all_df = pd.merge(tag_df, top_20, on='tag__tag_name')

    fig = px.bar(
        all_df, 
        x='count',
        y='tag__tag_name',
        title='Tag frequency in All posts',
        labels={'count':'Frequency', 'tag__tag_name':'Tags'},
        color='tag__tag_name'
    )
    fig.update_layout(
        height = 1000,
        yaxis={'categoryorder':'total ascending'},  
        yaxis_title='Tags',   
        paper_bgcolor='#333', 
        plot_bgcolor='#333', 
        font = {'color':'white'},  
    )

    plot_div = fig.to_json()

    print('Sending plot data...')
    return JsonResponse({'plot_div': plot_div})
  • model에서 데이터 불러와서 df로 변환할 때 pd.DataFrame.from_records(queryset_dict)
  • model에서 값을 가져올 때 ForeignKey라면 부모 테이블__부모 테이블에서의 컬럼명
  • Plotly 는 그래프를 JSON 형식으로 생성할 수 있음 -> 클라이언트에서 JSON 받아서 그래프 생성
  • JSON을 보낼 땐 JsonResponse

urls.py

1
path('visualization_all/', views.all_chart, name='all_chart'),  # 추가
  • js에서 AJAX서버가 방문 할 url
  • 직접 이 주소로 들어가면 JSON 파일이 출력됨

home.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
    <main>
        <section class="dashboard">
            <div class="dashboard-title">
                <h1>IT 직군 트렌드 분석</h1>
            </div>
            <div class="dashboard-buttons">
                <button id="all-button">전체</button>
                <button id="company-button">기업별</button>
            </div>
            <div id="company-list"></div>     <!-- 기업 별 시각화 버튼 -->
            <div id="chart-container"></div>  <!-- 시각화 -->
        </section>
        ...
    </main>
</body>
  • all-button 버튼을 누르면 chart-container에 그래프를 띄우게 할 것

home.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
document.addEventListener("DOMContentLoaded", function () {
    //대시보드 이벤트 처리
    const allButton = document.getElementById("all-button");
    const chartContainer = document.getElementById("chart-container");

    // 전체 버튼을 클릭했을 때의 처리
    allButton.addEventListener("click", function () {
        companyList.innerHTML="";
        chartContainer.innerHTML="";
        fetch ("visualization_all/")
            .then(response => response.json())
            .then(data => {
                var fig = JSON.parse(data.plot_div);
                Plotly.newPlot('chart-container', fig.data, fig.layout);
            })
            .catch(error => console.error('Error: ', error))
    });
    ...
});

JS - document

  • document 객체
    • 웹페이지 그 자체
    • 웹페이지에 존재하는 HTML 요소에 접근하기 위해서는 반드시 document 객체부터 시작해야 함
  • getElementsById
    • 해당 아이디의 요소를 선택!
    • ID는 유일함
    • ClassName으로 검색한다면 해당 클래스에 속한 요소를 모두 선택함
  • createElement(HTML 요소)
    • 지정된 HTML 요소 생성

JS - Fetch

  • Fetch API
    • HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스 제공
    • 기존 XMLHttpRequest를 대체
  • fetch()
    • 첫번째 인자로 URL, 두번째 인자로 옵션 객체를 받음
    • 옵션 객체에는 HTTP 방식, HTTP 요청 header, HTTP 요청 body 등을 설정할 수 있음
    • 응답 객체로부터는 HTTP 응답 상태, HTTP 응답 header, HTTP 응답 body 등을 읽어올 수 있음
    • POST method로 폼 등을 사용해 데이터를 만들어 보내거나 비밀번호 등 개인정보를 보낼 수 있음
      • body 옵션에는 요청 전문을 JSON 포맷으로 넣어줌
  • 본 프로젝트에서는
    • 받은 json으로 Plotly.newPlot() 함수를 이용하여 그래프 생성
      • fig.data, fig.layout은 그대로 써야함!
    • html로도 전달이 가능한 것 같은데, json을 사용하면 클라이언트 측에서 그래프를 더 유연하게 제어할 수 있음

결과

  • [전체] 버튼을 눌렀을 때

스크린샷 2023-11-11 오후 2 14 27
스크린샷 2023-11-11 오후 2 15 22

3. 기업 별 태그 빈도수 시각화

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def company_chart(request, company):
    tag = pd.DataFrame.from_records(Company_Tag.objects.filter(company__company_name='{}'.format(company)).values('tag__tag_name'))
    count = pd.DataFrame.from_records(Company_Tag.objects.filter(company__company_name='{}'.format(company)).values('tag__tag_name', 'count'))
    company_df = pd.merge(tag, count, on='tag__tag_name')
    company_df.drop(company_df[company_df['tag__tag_name'] == ''].index, inplace=True)
    top_20 = company_df.sort_values(by='count', ascending=False).head(20)
    fig = px.bar(
        top_20,
        x='count',
        y='tag__tag_name',
        title='Tag frequency in {} posts'.format(company),
        labels={'count':'Frequency', 'tag__tag_name':'Tags'},
        color='tag__tag_name'
    )
    fig.update_layout(
        height = 1000,
        yaxis={'categoryorder':'total ascending'},  # 빈도수가 높은 순으로 정렬
        yaxis_title='Tags',  # y축 제목 설정
        paper_bgcolor='#333', # 차트 바깥쪽 배경색
        plot_bgcolor='#333', # 차트 안쪽 배경색
        font = {'color':'white'},  # 전체 글자(폰트) 색상
    )

    company_div = fig.to_json()

    print('Sending plot data...')
    return JsonResponse({'company_div': company_div})

urls.py

1
path('company_chart/<str:company>', views.company_chart, name='company_chart'), # 추가

home.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
    <main>
        <section class="dashboard">
            <div class="dashboard-title">
                <h1>IT 직군 트렌드 분석</h1>
            </div>
            <div class="dashboard-buttons">
                <button id="all-button">전체</button>
                <button id="company-button">기업별</button>
            </div>
            <div id="company-list"></div>     <!-- 기업 별 시각화 버튼 -->
            <div id="chart-container"></div>  <!-- 시각화 -->
        </section>
        ...
    </main>
</body>

home.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
document.addEventListener("DOMContentLoaded", function () {
    const companyButton = document.getElementById("company-button");
    const companyList = document.getElementById("company-list");

    // 기업별 버튼을 클릭했을 때의 처리
    companyButton.addEventListener("click", function () {
        chartContainer.innerHTML="";
        companyList.innerHTML = `
            <div class="company-list">
                <button id="gangnam-button">강남언니</button>
                <button id="naver-button">네이버</button>
                <button id="danggn-button">당근마켓</button>
                <button id="devocean-button">데보션</button>
                <button id="line-button">라인</button>
                <button id="musinsa-button">무신사</button>
                <button id="bank-button">뱅크샐러드</button>
                <button id="socar-button">쏘카</button>
                <button id="watcha-button">왓챠</button>
                <button id="yogiyo-button">요기요</button>
                <button id="woowa-button">우아한형제들</button>
                <button id="est-button">이스트소프트</button>
                <button id="kakao-button">카카오</button>
                <button id="kakaoenter-button">카카오 엔터프라이즈</button>
                <button id="kakaopay-button">카카오페이</button>
                <button id="coupang-button">쿠팡</button>
                <button id="hc-button">하이퍼커넥트</button>
                <button id="skplanet-button">SK플래닛</button>
            </div>
        `;
    });

    // 특정기업 버튼 클릭했을시 처리
    companyList.addEventListener("click", function (event) {
        if (event.target.id === "gangnam-button") {
            fetch ("company_chart/강남언니")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "naver-button") {
            fetch ("company_chart/네이버")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "danggn-button") {
            fetch ("company_chart/당근")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "devocean-button") {
            fetch ("company_chart/데보션")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))
            
        } else if (event.target.id === "line-button") {
            fetch ("company_chart/라인")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "musinsa-button") {
            fetch ("company_chart/무신사")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "bank-button") {
            fetch ("company_chart/뱅크샐러드")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "socar-button") {
            fetch ("company_chart/쏘카")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "watcha-button") {
            fetch ("company_chart/왓챠")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "yogiyo-button") {
            fetch ("company_chart/요기요")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "woowa-button") {
            fetch ("company_chart/우아한형제들")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "est-button") {
            fetch ("company_chart/이스트소프트")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "kakao-button") {
            fetch ("company_chart/카카오")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "kakaoenter-button") {
            fetch ("company_chart/카카오엔터프라이즈")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "kakaopay-button") {
            fetch ("company_chart/카카오페이")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "coupang-button") {
            fetch ("company_chart/쿠팡")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "hc-button") {
            fetch ("company_chart/하이퍼커넥트")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))

        } else if (event.target.id === "skplanet-button") {
            fetch ("company_chart/SK플래닛")
                .then(response => response.json())
                .then(data => {
                    var fig = JSON.parse(data.company_div);
                    Plotly.newPlot('chart-container', fig.data, fig.layout);
                })
                .catch(error => console.error('Error: ', error))
        }
        
    });
});

스크린샷 2023-11-11 오후 2 21 01
스크린샷 2023-11-11 오후 2 21 34

4. 아쉬운 점

  • 기업 버튼 리스트를 모델에서 불러와서 동적으로 해보려 했는데 시간이 부족하여 우선 갖고 있는 리스트를 직접 넣었다.
  • 시각화를 두 가지 밖에 하지 못한 점이 아쉽다. 더 나아가 유저의 활동 (좋아요, 조회수 등)과도 연동해서 시각화를 해보면 좋을 것 같다.
  • 사소한 실수로 시간을 너무 많이 잡아먹은 것이 아쉽다. 또한, 아직 웹에 대한 이해가 부족해서 시간이 오래 걸렸던 점도 아쉽다. 캠프와 병행하며 웹 공부를 더 해봐도 좋을 것 같다.

5. 사소한 실수

  • views.py 에서 시각화 함수를 작성할 때 fig.show() 를 추가하면 <div> 안에 들어가는게 아니라 새로운 창에 그래프가 뜬다.. 이것 때문일 줄은 몰랐다. :(
  • model에서 값을 가져올 때 all(), filter(), values(), values_list() 등으로 가져올 때 모두 타입이 다르다. 이것을 정리하면 좋을듯! => ORM QuerySet
This post is licensed under CC BY 4.0 by the author.