Dash + Plotly 사용법/튜토리얼
Programming Set-Up

Dash + Plotly 사용법/튜토리얼

일시불

Table of Contents

이 포스팅은 숙명여대에서 진행한 빅데이터캠프의 데이터 시각화 과정의 강의자료 중 일부입니다. 강의 의뢰는  아래 카톡으로 연락주세요.
서울대 현직자에게 배우는 코딩/과제/프로젝트
❤️모든 코딩 문제 해결! #파이썬 #데이터분석 #코딩 #프로그래밍 #컴공과제 #코딩과외

dash를 시작합니다

dash는 프론트엔드로 plotly.jsReact를 사용하고 백엔드로 Flask를 사용하는 데이터 시각화 프레임워크입니다. dash를 사용하면 파이썬 하나만으로 데이터 분석 결과를 간편하게 웹 서비스로 구현할 수 있습니다.

Manufacturing SPC Dashboard

HTML 기초

dash 는 HTML을 사용해 화면을 구성합니다. 여기에서는 HTML의 기본 문법을 소개합니다.

Untitled

위에서 보신 멋진 스케치에서 해골 그림을 잘 기억하세요. 웹에서 HTML(HyperText Markup Language)은 데이터의 전체적인 "골격"을 나타냅니다. HTML은 골격을 나타낸다는 의미에 부합하는 구조를 가지고 있습니다. 즉 "head", "body", "footer" 3개의 태그로 구분됩니다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>
💡 CSS(Cascade Style Sheet)는 골격 외부를 꾸미는 역할을 하고, 실제로 움직이도록 하는 건 자바스크립트가 담당합니다.

index.html

이제 VS Code에서 index.html 이라는 파일을 하나 생성합니다. 이 파일은 모든 웹사이트의 메인 페이지가 되는 파일입니다. 만일 우리가 https://google.com이라는 주소로 접속하면, 실제로는 https://google.com/index.html로 접속하는 것과 동일합니다. 직접 해보세요!

Untitled
Untitled

이제 html 파일에 아래 코드를 입력합니다.

<!DOCTYPE html> # Standard type document
<html></html> # root element

HTML에서는 코드를 <></>단위로 구분하고, <> 가 한번 나오면 반드시 그에 해당하는 </> 가 쌍으로 따라와야 합니다. 이를 관용적으로 "태그를 열고 닫는다" 라고 말합니다.

이제 이 파일을 브라우저에서 열면 지금은 빈 화면이지만,  HTML 코드가 브라우저에 렌더링되는 것을 볼 수 있습니다.

<html> 태그 아래에 <head> 태그를 아래와 같이 입력합니다.

<head>
	<title>Document</title>
	<meta charset="utf-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>

head 태그는 웹페이지의 중요한 정보들을 담고 있는 영역입니다. 이를 메타데이터(metadata)라고 부르며, <meta> 태그로 표기합니다. 위 head 태그에서 알 수 있는 정보는 다음과 같습니다.

  • 웹 페이지의 타이틀(<title>)
  • 웹 페이지의 인코딩 방식("utf-8")
  • 브라우저 호환성 정보
  • Viewport 정보. 웹 페이지의 콘텐츠가 화면에 어떻게 표시될지를 정의합니다.
  • 나중에 모바일 화면을 다루게 될 때 Viewport를 자세히 배웁니다.

body

이제 head 태그 다음에 body 태그를 넣습니다.

<!DOCTYPE html>
<html lang="en">
		<head>
		    <meta charset="UTF-8">
		    <meta http-equiv="X-UA-Compatible" content="IE=edge">
		    <meta name="viewport" content="width=300, initial-scale=1.0">
		    <title>Document</title>
		</head>
		<body>
		    
		</body>
</html>

body 태그는 화면에 표시되는 요소들을 담고 있습니다. 여기서 기본적인 HTML 엘리먼트 몇 가지를 소개합니다.

먼저 헤딩 태그입니다. 헤딩은 텍스트 요소의 수준을 나타냅니다. 헤딩 1부터 6까지 순서가 커질수록 아래 수준의 요소를 의미합니다. 이를 Semantic markup 이라고 부릅니다. 이 시맨틱 마크업이 중요한 이유는 웹페이지의 구조를 직관적으로 파악하기 쉽기 때문입니다. 즉 헤딩 숫자가 낮을수록 가장 핵심적인 내용을 포함하고 있다는 뜻입니다.

<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>

다음으로 <p> 태그는 문단을 나타냅니다. 그리고 이미지를 표시하는 태그는 <img> 로, 닫는 태그가 필요 없습니다. src 라는 속성이 있어서 여기에 이미지 주소를 담기 때문입니다. 속성에 관해서는 다음 섹션에서 자세히 다루겠습니다.

마지막으로 <div> 태그는 어떤 요소를 논리적으로 구분하기 위한 태그입니다. 실제로 화면에는 아무것도 표시되지 않지만, div 태그 안에 들어가 있는 요소들을 묶어줌으로써 이해하기 쉬운 코드를 만듭니다.

위의 내용을 모두 포함한 HTML 코드는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=300, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>Heading 1</h1>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h5>Heading 5</h5>
    <h6>Heading 6</h6>

    <div id="article">
        <p>This is paragraph.</p>
        <img
            src="https://www.apple.com/105/media/us/macbook-pro-16/2019/fa0563a0-8534-4e01-a62a-081b87805fea/anim/hero/large/large_0118.jpg">
    </div>
</body>

</html>
💡 VS Code에서는 매우 편리한 기능을 제공합니다. html 파일에 `!` 를 입력하고 엔터를 입력하면 표준 HTML 코드를 만들어줍니다.

img 태그

위에서 img 태그에 대해서 짧게 설명하고 넘어갔는데요. 지금까지 배운 HTML 태그들은 <something> 과 같이 표현되었습니다. 그런데 웹에 이미지를 표시하기 위해서 만든 img 태그는, 이미지를 어디서 가져와야 할지를 나타내는 src 라는 부분이 들어있습니다. 이를 속성(attribute)이라고 부릅니다. 속성은 태그 내부에 어떤 값을 넣고자 할때 사용하는 문법입니다. 태그마다 어떤 속성을 가지고 있고, 어떤 값을 넣을 수 있는지는 웹 표준을 다루고 있는 MDN에서 찾을 수 있습니다.

HTML: Hypertext Markup Language | MDN

MDN에서 우측 상단의 검색창에 찾아보고 싶은 태그를 입력하면 됩니다. 우리는 img 태그를 찾아보겠습니다.

Untitled

특성(속성) 섹션에서 우리가 원하는 정보를 찾을 수 있습니다.

Untitled

예를 들어 이미지의 크기를 바꾸려면 width 의 값을 %로 입력하면 됩니다. img 태그에 width=10% 를 추가해 보세요.

ol/ul/li

다음으로 다룰 태그는 목록을 만드는 태그입니다. 여기서 말하는 목록(list)란 다음과 같은 두 가지 경우가 있습니다.

  1. 순서가
  2. 있는
  3. 목록
  • 순서가
  • 없는
  • 목록

각각은 다음과 같이 만들 수 있습니다. (HTML 버튼을 눌러보세요)

ul 은 순서가 없는 목록, Unordered list입니다. ol 은 순서가 있는 목록, Ordered list 입니다. 둘 다 하위 태그로 li 를 가지고 있습니다. 이와 같은 구조를 부모-자식 관계라고 부르고, ol/ul 태그를 부모 태그, li 태그를 자식 태그라고 부릅니다.

ol 태그의 경우, 안에 있는 li 태그의 갯수에 맞추어 목록의 순서가 늘어나기 때문에 편리합니다.

a 태그

마지막으로 다룰 태그는 앵커(anchor) 태그인 a 태그입니다. 이 태그는 다른 태그에 링크를 걸 수 있는 태그입니다. 아래 예제에서 Google이라는 텍스트와 애플 로고를 누르면 각 회사의 홈페이지로 이동합니다.

a 태그는 href 라는 속성에 링크를 걸 수 있습니다. 참고로 href 란 Hypertext reference라는 뜻으로, HTML의 Hypertext와 같은 단어입니다. 웹은 항상 이 링크를 이용해서 다른 페이지로 이동하기 때문에, 앵커 태그야말로 웹의 가장 핵심적인 태그라고 할 수 있습니다.

프로젝트 파일 만들기

현재 폴더에 index.html 을 만들고, 아래 코드를 입력해주세요.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple website</title>
    <link rel="stylesheet" href="main.css">
  </head>
  <body>
    <h1>Task List</h1>
    <p id="msg">Current tasks:</p>
    <ul>
      <li class="list">Add visual styles</li>
      <li class="list">Add light and dark themes</li>
      <li>Enable switching the theme</li>
    </ul>
  </body>
</html>

그 다음 브라우저에서 index.html 파일을 열어줍니다.

CSS 기초

CSS(Cascading Style Sheets)는 웹 페이지를 보기 좋게 만드는 역할을 합니다. "보기 좋게"라는 말은 사용자가 좀더 쉽고 편리하게 웹 페이지를 사용할 수 있도록 도와준다는 의미입니다. 또한 CSS를 사용하면 반응형 웹 디자인을 만들거나 애니메이션을 추가해 좀더 역동적인 웹 페이지를 만드는 것도 가능합니다.

External CSS

index.css 파일을 현재 폴더에 만든 다음,  아래와 같이 index.html 파일에서 css를 불러와 주겠습니다.

...
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Task Timeline</title>
  <link rel="stylesheet" href="index.css">
...

CSS rules

CSS는 다음과 같은 모양으로 코드를 작성합니다. 가장 먼저 스타일이 적용될 HTML 요소를 적고, {} 안에 스타일을 표기하면 됩니다.

body {
    font-family: monospace;
}

ul {
    font-family: helvetica;
}

이렇게 표현된 CSS 코드의 각 부분을 부르는 명칭은 다음과 같습니다.

  • selector: ul {}<ul>
  • property name: font-family
  • value: helvetica

Selectors

스타일이 적용될 요소를 지정하는 방법은 총 3가지가 있습니다.

  • 요소 태그
  • class name
  • id

아래 예제에서는 각 방법을 예시로 보여주고 있습니다.

  • .list : 클래스 selector
  • #msg : id selector
li {
  list-style: circle;
}

.list {
  list-style: square;
}

#msg {
  font-family: helvetica;
}

클래스란, 같은 계층에 존재하는 요소들을 묶어주기 위해 사용됩니다.우리의 HTML 코드 예시를 살펴봅시다.

    <ul>
      <li class="list">Add visual styles</li>
      <li class="list">Add light and dark themes</li>
      <li>Enable switching the theme</li>
    </ul>

첫 번째와 두번째의 <li>class="list" 를 가지고 있지만 세번째는 가지고 있지 않습니다.  위 CSS를 index.css 에 추가한 다음, 실제 브라우저 화면을 보면 아래와 같이 나타납니다.

id는 어떤 요소를 특정하기 위해 사용되는 식별자입니다. 태그 이름이 같더라도, id가 다르면 스타일을 다르게 주는 것이 가능합니다.

다시 브라우저 화면을 살펴보면, 폰트가 요소마다 다르게 나타나는 것을 알 수 있습니다.  body 에서 정의한 스타일이 h1 에도 적용되는 것을 눈치채셨나요? 이처럼 CSS는 "연쇄적으로(cascade)" 적용됩니다. 반면, li 는 명시적으로 CSS를 정의해주었기 때문에 다르게 나타나는 것을 알 수 있습니다!

적용된 CSS 찾아보기

  1. 브라우저를 열고 개발자 도구를 실행합니다. F12 또는 Ctrl+Shift+I 또는 맥의 경우 Option+Command+I를 입력하면 됩니다.
  2. 요소 Elements 탭을 선택합니다.
  3. 스타일 Styles 탭을 선택합니다.
  4. HTML 요소들을 몇 개 선택해보고, 각각에 적용된 CSS를 확인합니다.
  5. <li> 요소의 경우, font-family: helvetica;가 적용되어 <body> 요소와는 스타일이 다른 것을 확인할 수 있습니다.

예제 앱 실행하기

pip3 install dash

이제 드디어 dash로 넘어왔습니다. dash는 plotly 그래프를 웹에서 표현할 수 있도록 하는 라이브러리입니다. plotly로 만든 fig 그래프들을 사용해 웹페이지를 만들어줍니다. 여기서부터는 노트북이 아닌 파이썬 모듈(app.py ) 파일을 만들어 실습을 진행해주세요.

💡 아래 예제 코드를 보면, `dash.Dash` 클래스가 플라스크의 `Flask` 객체와 유사하게 서버 인스턴스 `app` 을 생성하는 패턴을 가지고 있습니다.

먼저 필요한 패키지를 불러옵니다.

from dash import dcc, html, Dash
import plotly.express as px
import pandas as pd

app = dash.Dash(__name__)

그리고 샘플 데이터셋을 만들어 보겠습니다.

df = pd.DataFrame(
    {
        "Fruit": [
            "Apples",
            "Oranges",
            "Bananas",
            "Apples",
            "Oranges",
            "Bananas",
        ],
        "Amount": [4, 1, 2, 2, 4, 5],
        "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"],
    }
)

다음으로는 plotly express의 바 그래프를 사용해 위 데이터를 나타냅니다.

fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group")

여기서부터가 중요한데, dash는 HTML 요소들을 app.layout에 넣어서 화면 배치를 구성합합니다. 이때 일반 HTML 요소가 아닌 plotly 그래프를 추가하려면 dcc.Graph 클래스를 이용합니다.

app.layout = html.Div(
    children=[
        html.H1(children='Hello Dash'),
        html.Div(
            children='''
        Dash: A web application framework for your data.
    '''
        ),
        dcc.Graph(id='example-graph', figure=fig),
    ]
)

마지막으로 서버를 실행하는 코드를 추가합니다. 포트 번호를 별도로 지정할 수도 있지만, 그대로 실행하면 [https://localhost:8050](http://localhost:8050) 에서 서버가 시작됩니다. 여기서 debug=True 를 설정하면, 디버그 모드로 서버가 실행됩니다. 서버 성능이 떨어지고 보안에 취약해지지만, 대신 파이썬 코드가 수정될 때마다 화면이 자동으로 업데이트되기 때문에 편리합니다.

if __name__ == '__main__':
    app.run_server(debug=True)

지금까지의 전체 코드는 다음과 같습니다.

from dash import dcc, html, Dash
import plotly.express as px
import pandas as pd

app = Dash(__name__)

df = pd.DataFrame(
    {
        "Fruit": [
            "Apples",
            "Oranges",
            "Bananas",
            "Apples",
            "Oranges",
            "Bananas",
        ],
        "Amount": [4, 1, 2, 2, 4, 5],
        "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"],
    }
)

fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group")

app.layout = html.Div(
    children=[
        html.H1(children='Hello Dash'),
        html.Div(
            children='''
        Dash: A web application framework for your data.
    '''
        ),
        dcc.Graph(id='example-graph', figure=fig),
    ]
)

if __name__ == '__main__':
    app.run_server(debug=True)

이제 [app.py](http://app.py) 를 실행합니다.

$ python app.py
...Running on http://127.0.0.1:8050/ (Press CTRL+C to quit)

코드를 실행하면 웹브라우저에 아래와 같은 화면이 나타납니다. 그래프 부분을 plotly 로 만들었기 때문에, 여전히 인터랙션이 가능합니다.

레이아웃(layout)

Dash 어플리케이션은 두가지 요소로 구성되어 있습니다.

  • 레이아웃, 페이지가 화면에 어떻게 나타나는지를 정의하는 요소
  • 콜백, 상호작용 요소

여기서 레이아웃은 HTML 태그를 나타내는 html 모듈과, 미리 만들어져 있는 고급 기능인 dcc 모듈 두가지로 나뉘어져 있습니다.

HTML 컴포넌트(html)

html 모듈의 경우, 모든 HTML 태그들이 어트리뷰트와 함께 구현되어 있습니다. 모든 HTML 컴포넌트의 목록은 아래 링크를 참고하세요.

Dash HTML Components

💡 React의 컴포넌트를 생각하시면 됩니다.

HTML 컴포넌트와 HTML 코드의 관계

html 컴포넌트를 사용해 페이지를 아래와 같이 구성할 수 있습니다.

from dash import html

html.Div(
    [
        html.H1('Hello Dash'),
        html.Div(
            [
                html.P('Dash converts Python classes into HTML'),
                html.P(
                    "This conversion happens behind the scenes"
                    " by Dash's JavaScript front-end"
                ),
            ]
        ),
    ]
)

이 파이썬 코드는 나중에 웹페이지에서 아래와 같이 나타나게 됩니다. 즉 html. 객체가 HTML 태그 하나로 변환되는 것과 동일합니다.

<div>
    <h1>Hello Dash</h1>
    <div>
        <p>Dash converts Python classes into HTML</p>
        <p>This conversion happens behind the scenes by Dash's JavaScript front-end</p>
    </div>
</div>

HTML 컴포넌트에 CSS 적용하기

HTML과 마찬가지로, CSS도 dash html에 적용이 가능합니다. CSS에서 클래스, id, 프로퍼티를 배운 것 기억나시나요? 이 요소들이 dash에도 동일하게 구현되어 있습니다.

  • 클래스 이름은 className 에 입력합니다. id는 동일하게 id 입니다.
  • 프로퍼티는 style 파라미터에 딕셔너리로 입력합니다. 이때 캐멀 케이스(camelCase) 이름규칙으로 작성하면 됩니다. 픽셀 값인 경우는 px 없이 숫자만 입력합니다.

위 규칙에 맞춰서 작성한 아래 파이썬 코드를 참고하세요.

from dash import html

html.Div([
    html.Div('Example Div', style={'color': 'blue', 'fontSize': 14}),
    html.P('Example P', className='my-class', id='my-p-element')
], style={'marginBottom': 50, 'marginTop': 25})

이 코드는 결과적으로 아래와 같이 변환됩니다.

<div style="margin-bottom: 50px; margin-top: 25px;">

    <div style="color: blue; font-size: 14px">
        Example Div
    </div>

    <p class="my-class", id="my-p-element">
        Example P
    </p>

</div>

HTML 표 만들기

다음으로는 좀더 복잡한 예제로 html 표를 만드는 걸 살펴보겠습니다. 표에는 아래 요소들이 사용됩니다.

  • html.Table
  • html.Thead
  • html.Tbody
  • html.Tr
  • html.Td
from dash import Dash, html
import pandas as pd

df = pd.read_csv(
    'https://gist.githubusercontent.com/chriddyp/c78bf172206ce24f77d6363a2d754b59/raw/c353e8ef842413cae56ae3920b8fd78468aa4cb2/usa-agricultural-exports-2011.csv'
)

def generate_table(dataframe, max_rows=10):
    return html.Table(
        [
            html.Thead(html.Tr([html.Th(col) for col in dataframe.columns])),
            html.Tbody(
                [
                    html.Tr(
                        [
                            html.Td(dataframe.iloc[i][col])
                            for col in dataframe.columns
                        ]
                    )
                    for i in range(min(len(dataframe), max_rows))
                ]
            ),
        ]
    )

app = Dash(__name__)

app.layout = html.Div(
    [html.H4(children='US Agriculture Exports (2011)'), generate_table(df)]
)

if __name__ == '__main__':
    app.run_server(debug=True)

위 코드를 실행해서 나온 표의 HTML 코드를 살펴보면 아래와 같습니다.

<table>
  <thead>
    <tr>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td></td>
    </tr>
  </tbody>
</table>

Dash core component(dcc)

dcc 는 미리 만들어져 있는 고급 컴포넌트들로, 컨트롤러나 그래프와 같은 요소들을 포함하고 있습니다. 전체 컴포넌트의 레퍼런스는 아래 문서에서 확인하세요.

Dash Core Components

dcc.Graph

dccplotly 그래프를 표현하는데 사용됩니다. express나 graph object 두 가지를 모두 지원합니다. 아래 예제에서는 go 를 사용해 그래프를 만들었습니다.

from dash import Dash, dcc, html
import plotly.graph_objs as go

fig = go.Figure(data=[go.Scatter(x=[1, 2, 3], y=[4, 1, 2])])

app = Dash(__name__)
app.layout = html.Div(
    dcc.Graph(id='example-graph', figure=fig),
)

if __name__ == '__main__':
    app.run_server(debug=True)

마크다운(Markdown)

HTML 마크업 대신, 마크다운을 활용할 수도 있습니다. 마크다운이란 프로그래밍 언어로 문서를 만들 수 있는 방법으로, 개발 분야에서 널리 사용됩니다. 마크다운에 대해 더 자세히 배우고 싶다면 아래 글을 참고하세요.

가볍게 배우는 Markdown (+ LaTex) with Typora
0. Typora 현재 사용하고 있는 마크다운(Markdown) 편집기가 없다면 Typora를 추천합니다. A new way to read & write MarkdownTypora is a cross-platform minimal markdown editor, providing seamless experience for both markdown readers and writers. Typora는 매우 편리하고 심플한, 그리고 예쁜 마크다운 편집기다. 마크다운 편집이 아니더라도 메모장으로도 자주 사용한다. 특히 코드블럭과 수식도

아래 예제에서 마크다운으로 작성된 문자열 markdown_text  를 만든 다음, 이를 dcc.Markdown 에 넣어주면 됩니다.

from dash import Dash, html, dcc

markdown_text = '''
# h1 Heading
## h2 Heading
### h3 Heading

***

## Emphasis

**This is bold text**

*This is italic text*

~~Strikethrough~~

## Blockquotes
> Blockquotes

## Lists
Unordered

+ Create a list by starting a line with `+`, `-`, or `*`
+ Sub-lists are made by indenting 2 spaces:
  - Marker character change forces new list start:
    * Ac tristique libero volutpat at
+ Very easy!

Ordered

1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit
3. Integer molestie lorem at massa

## Code

Inline `code`


``` js
var foo = function (bar) {
  return bar++;
};

console.log(foo(5));
```

## Tables

| Option | Description |
| ------ | ----------- |
| data   | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext    | extension to be used for dest files. |
'''

app = Dash(__name__)
app.layout = html.Div([dcc.Markdown(markdown_text)])


if __name__ == '__main__':
    app.run_server(debug=True)

다음은 드롭다운 메뉴를 만드는 방법입니다. dcc.Dropdown 은 2가지 요소를 만들 수 있습니다. 하나는 단일 선택 드롭다운, 나머지는 다중 선택 드롭다운입니다. 다중 선택을 만드려면 파라미터로 multi=True 를 주면 됩니다.

여기서 options 는 드롭다운에서 선택할 수 있는 선택지를 의미합니다. value 는 기본으로 선택되어 있는 값으로, 명시하지 않으면 빈 값으로 전달됩니다.

import dash
from dash import dcc, html

app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Div(
            [
                html.Label('Dropdown'),
                dcc.Dropdown(
                    options=[
                        {'label': 'New York City', 'value': 'NYC'},
                        {'label': u'Montréal', 'value': 'MTL'},
                        {'label': 'San Francisco', 'value': 'SF'},
                    ],
                    value='MTL',
                ),
                html.Br(),
                html.Label('Multi-Select Dropdown'),
                dcc.Dropdown(
                    options=[
                        {'label': 'New York City', 'value': 'NYC'},
                        {'label': u'Montréal', 'value': 'MTL'},
                        {'label': 'San Francisco', 'value': 'SF'},
                    ],
                    value=['MTL', 'SF'],
                    multi=True,
                ),
            ],
            style={'padding': 10, 'flex': 1},
        ),
    ],
    style={'display': 'flex', 'flex-direction': 'row'},
)

if __name__ == '__main__':
    app.run_server(debug=True)

Radioitem

라디오 요소도 만들어볼 수 있습니다.

import dash
from dash import dcc, html

app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Label('Radio Items'),
        dcc.RadioItems(
            options=[
                {'label': 'New York City', 'value': 'NYC'},
                {'label': u'Montréal', 'value': 'MTL'},
                {'label': 'San Francisco', 'value': 'SF'},
            ],
            value='MTL',
        ),
    ]
)

if __name__ == '__main__':
    app.run_server(debug=True)

checkbox, input, slider

체크박스와 텍스트 입력, 그리고 슬라이더도 구현되어 있습니다.

import dash
from dash import dcc, html

app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Div(
            children=[
                html.Label('Checkboxes'),
                dcc.Checklist(
                    options=[
                        {'label': 'New York City', 'value': 'NYC'},
                        {'label': u'Montréal', 'value': 'MTL'},
                        {'label': 'San Francisco', 'value': 'SF'},
                    ],
                    value=['MTL', 'SF'],
                ),
                html.Br(),
                html.Label('Text Input'),
                dcc.Input(placeholder="010-1234-5678", value='MTL', type='text'),
                html.Br(),
                html.Label('Slider'),
                dcc.Slider(
                    min=0,
                    max=9,
                    marks={
                        i: f'Label {i}' if i == 1 else str(i)
                        for i in range(1, 6)
                    },
                    value=5,
                ),
            ],
            style={'padding': 10, 'flex': 1},
        ),
    ],
    style={'display': 'flex', 'flex-direction': 'row'},
)

if __name__ == '__main__':
    app.run_server(debug=True)

버튼

마지막으로 간단한 버튼을 만들어 보겠습니다.

import dash
from dash import html, dcc

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Div(dcc.Input(type='text')),
        html.Button('Submit'),
    ]
)

if __name__ == '__main__':
    app.run_server(debug=True)

콜백(callbacks)

이번에는 dash에서 가장 복잡한 개념인 콜백에 대해서 알아보겠습니다. 콜백은 콜백 함수(callback functions)을 의미하는데, 컴포넌트의 프로퍼티가 변경될 때마다 자동으로 호출되는 함수를 의미합니다. 따라서 컴포넌트의 상태에 따라 화면의 요소를 유기적으로 업데이트하는 데 사용됩니다.

input 예제

위에서 배웠던 dcc.Input 컴포넌트를 사용한 예제를 살펴보겠습니다.

from dash import Dash, dcc, html, Input, Output

app = Dash(__name__)

app.layout = html.Div([
    html.H6("Change the value in the text box to see callbacks in action!"),
    html.Div([
        "Input: ",
        dcc.Input(id='my-input', value='initial value', type='text')
    ]),
    html.Br(),
    html.Div(id='my-output'),

])

@app.callback(
    Output(component_id='my-output', component_property='children'),
    Input(component_id='my-input', component_property='value')
)
def update_output_div(input_value):
    return f'Output: {input_value}'

if __name__ == '__main__':
    app.run_server(debug=True)

이 코드의 실행 과정을 단계별로 설명해 보겠습니다.

  1. 애플리케이션의 입력 및 출력은 @app.callback 데코레이터의 파라미터로 정의됩니다.
  2. 대시에서 애플리케이션의 입력과 출력은 단순히 특정 구성 요소의 속성입니다. 이 예시에서 입력은 my-input ID를 가진 구성 요소의 value 속성입니다. 출력은 my-output ID를 가진 컴포넌트의 children 프로퍼티입니다.
  3. 입력 프로퍼티가 변경될 때마다 콜백 데코레이터가 래핑하는 함수가 자동으로 호출됩니다. 대시는 이 콜백 함수에 입력 속성의 새 값을 인수로 제공하고, 대시는 이 함수가 반환한 값으로 출력 구성 요소의 속성을 업데이트합니다.
  4. component_idcomponent_property 키워드는 선택 사항입니다. 해당 객체 각각에 대해 두 개의 인수만 있기 때문입니다. 이 예제에서는 명확성을 위해 포함했지만, 지금부터는 코드의 간결성과 가독성을 위해 생략합니다.
  5. 레이아웃에서 my_output 컴포넌트의 children 파라미터에 값을 설정하지 않았습니다. Dash 앱이 시작되면,  입력 컴포넌트의 초기 값으로 모든 콜백을 자동으로 호출합니다. 따라서 div 컴포넌트를 html.Div(id='my-output', children='Hello world')로 지정한 경우, 앱이 시작될 때 미리 지정한 “Hello world”가 “initial value”로 덮어씌워집니다.
💡 `dash.dependencies.Input` 객체와 `dcc.Input` 객체의 이름이 비슷하죠? 전자는 콜백 함수의 정의에 사용되며, 후자는 실제 입력을 받는 컴포넌트입니다.

Figure와 슬라이드 바

from dash import Dash, dcc, html, Input, Output
import plotly.express as px

import pandas as pd

df = pd.read_csv(
    'https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv'
)

app = Dash(__name__)

app.layout = html.Div(
    [
        dcc.Graph(id='graph-with-slider'),
        dcc.Slider(
            df['year'].min(),
            df['year'].max(),
            step=None,
            value=df['year'].min(),
            marks={str(year): str(year) for year in df['year'].unique()},
            id='year-slider',
        ),
    ]
)

@app.callback(
    Output('graph-with-slider', 'figure'), Input('year-slider', 'value')
)
def update_figure(selected_year):
    filtered_df = df[df.year == selected_year]

    fig = px.scatter(
        filtered_df,
        x="gdpPercap",
        y="lifeExp",
        size="pop",
        color="continent",
        hover_name="country",
        log_x=True,
        size_max=55,
    )

    fig.update_layout(transition_duration=500)

    return fig

if __name__ == '__main__':
    app.run_server(debug=True)

이 예제에서는 dcc.Slidervalue는 앱의 입력이고, 앱의 출력은 figure 입니다. dcc.Slidervalue가 변경될 때마다 dash는 새로운 값으로 콜백 함수인 update_figure를 호출합니다. 이 함수는 이 새로운 값으로 데이터프레임을 필터링하고, figure 객체를 생성해서 화면을 업데이트합니다.

이 예시에는 몇 가지 좋은 패턴이 있습니다:

  1. 앱 시작 시 데이터 프레임을 생성했는데요. df는 앱의 전역 변수이기 때문에 콜백 함수 내부에서 읽을 수 있습니다.
  2. 콜백 함수 내부가 아닌 앱 시작 시 쿼리 데이터를 로드함으로써 이 작업이 앱 서버가 시작될 때 한 번만 수행되도록 합니다. 사용자가 앱을 방문하거나 앱과 상호 작용할 때 해당 데이터 df는 이미 메모리에 있습니다. 가능하면 데이터 다운로드 또는 쿼리와 같이 비용이 많이 드는 초기화는 콜백 함수 내부가 아닌 앱의 글로벌 범위에서 수행해야 합니다.
  3. 콜백은 원본 데이터를 수정하지 않고, 필터링으로 데이터 프레임의 복사본만 생성합니다. 콜백은 범위를 벗어난 변수를 절대 수정해서는 안 됩니다. 콜백이 전역 상태를 수정하면, 한 사용자의 세션이 다음 사용자의 세션에 영향을 미칠 수 있으며, 앱이 여러 프로세스 또는 스레드에 배포된 경우 이러한 수정 사항은 세션 간에 공유되지 않습니다.
  4. 데이터가 시간에 따라 어떻게 변화하는지를 직관적으로 보여주기 위해 layout.transition으로 트랜지션 효과를 주고 있습니다. 트랜지션을 사용하면 차트가 마치 애니메이션처럼 한 상태에서 다음 상태로 부드럽게 업데이트될 수 있습니다.

다중 입력이 가능한 Dash 앱

대시에서 모든 출력은 여러 입력 컴포넌트를 가질 수 있습니다. 다음은 5개의 입력(두 개의 dcc.Dropdown 컴포넌트의 값 속성, 두 개의 dcc.RadioItems 컴포넌트, 하나의 dcc.Slider 컴포넌트)을 하나의 출력 컴포넌트(dcc.Graph 컴포넌트의 그림 속성)에 바인딩하는 간단한 예제입니다. app.callback이 출력 뒤에 5개의 입력 항목을 모두 나열하는 것을 주목하세요.

from dash import Dash, dcc, html, Input, Output
import plotly.express as px

import pandas as pd

app = Dash(__name__)

df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv')

app.layout = html.Div(
    [
        html.Div(
            [
                html.Div(
                    [
                        dcc.Dropdown(
                            df['Indicator Name'].unique(),
                            'Fertility rate, total (births per woman)',
                            id='xaxis-column',
                        ),
                        dcc.RadioItems(
                            ['Linear', 'Log'],
                            'Linear',
                            id='xaxis-type',
                            inline=True,
                        ),
                    ],
                    style={'width': '48%', 'display': 'inline-block'},
                ),
                html.Div(
                    [
                        dcc.Dropdown(
                            df['Indicator Name'].unique(),
                            'Life expectancy at birth, total (years)',
                            id='yaxis-column',
                        ),
                        dcc.RadioItems(
                            ['Linear', 'Log'],
                            'Linear',
                            id='yaxis-type',
                            inline=True,
                        ),
                    ],
                    style={
                        'width': '48%',
                        'float': 'right',
                        'display': 'inline-block',
                    },
                ),
            ]
        ),
        dcc.Graph(id='indicator-graphic'),
        dcc.Slider(
            df['Year'].min(),
            df['Year'].max(),
            step=None,
            id='year--slider',
            value=df['Year'].max(),
            marks={str(year): str(year) for year in df['Year'].unique()},
        ),
    ]
)

@app.callback(
    Output('indicator-graphic', 'figure'),
    Input('xaxis-column', 'value'),
    Input('yaxis-column', 'value'),
    Input('xaxis-type', 'value'),
    Input('yaxis-type', 'value'),
    Input('year--slider', 'value'),
)
def update_graph(
    xaxis_column_name, yaxis_column_name, xaxis_type, yaxis_type, year_value
):
    dff = df[df['Year'] == year_value]

    fig = px.scatter(
        x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
        y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
        hover_name=dff[dff['Indicator Name'] == yaxis_column_name][
            'Country Name'
        ],
    )

    fig.update_layout(
        margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest'
    )

    fig.update_xaxes(
        title=xaxis_column_name,
        type='linear' if xaxis_type == 'Linear' else 'log',
    )

    fig.update_yaxes(
        title=yaxis_column_name,
        type='linear' if yaxis_type == 'Linear' else 'log',
    )

    return fig

if __name__ == '__main__':
    app.run_server(debug=True)

이 예제에서 콜백은 dcc.Dropdown, dcc.Slider 또는 dcc.RadioItems 구성 요소의 value 속성이 변경될 때마다 실행됩니다. 콜백의 입력 파라미터는 지정된 순서대로 각 "입력" 속성의 현재 값입니다.

한 번에 하나의 '입력'만 변경되더라도(즉, 사용자는 주어진 순간에 하나의 드롭다운 값만 변경할 수 있음), 대시는 지정된 모든 '입력' 속성의 현재 상태를 수집하여 콜백 함수에 전달합니다. 이러한 콜백 함수는 항상 앱의 업데이트된 상태를 인지하고 있도록 보장됩니다.

다중 출력을 지원하는 Dash 앱

지금까지 작성한 모든 콜백은 하나의 Output 속성만 업데이트하고 있습니다. 하지만 한 번에 여러 개의 출력을 업데이트할 수도 있습니다. 업데이트하려는 모든 프로퍼티를 app.callback에 나열하고 콜백에서 해당 수만큼의 항목을 반환하면 됩니다.

from dash import Dash, dcc, html
from dash.dependencies import Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div(
    [
        dcc.Input(id='num-multi', type='number', value=5),
        html.Table(
            [
                html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]),
                html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]),
                html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]),
                html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]),
                html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]),
            ]
        ),
    ]
)

@app.callback(
    Output('square', 'children'),
    Output('cube', 'children'),
    Output('twos', 'children'),
    Output('threes', 'children'),
    Output('x^x', 'children'),
    Input('num-multi', 'value'),
)
def callback_a(x):
    return x**2, x**3, 2**x, 3**x, x**x

if __name__ == '__main__':
    app.run_server(debug=True)

체인 콜백이 있는 대시 앱

하나의 콜백 함수의 출력이 다른 콜백 함수의 입력이 될 수 있도록 출력과 입력을 함께 연결할 수도 있습니다.

이 패턴은 예를 들어 한 입력 컴포넌트가 다른 입력 컴포넌트의 사용 가능한 옵션을 업데이트하는 동적 UI를 만드는 데 사용할 수 있습니다. 다음은 간단한 예시입니다.

# -*- coding: utf-8 -*-
from dash import Dash, dcc, html, Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

all_options = {
    'America': ['New York City', 'San Francisco', 'Cincinnati'],
    'Canada': [u'Montréal', 'Toronto', 'Ottawa'],
}
app.layout = html.Div(
    [
        dcc.RadioItems(
            list(all_options.keys()),
            'America',
            id='countries-radio',
        ),
        html.Hr(),
        dcc.RadioItems(id='cities-radio'),
        html.Hr(),
        html.Div(id='display-selected-values'),
    ]
)

@app.callback(
    Output('cities-radio', 'options'), Input('countries-radio', 'value')
)
def set_cities_options(selected_country):
    return [{'label': i, 'value': i} for i in all_options[selected_country]]

@app.callback(Output('cities-radio', 'value'), Input('cities-radio', 'options'))
def set_cities_value(available_options):
    return available_options[0]['value']

@app.callback(
    Output('display-selected-values', 'children'),
    Input('countries-radio', 'value'),
    Input('cities-radio', 'value'),
)
def set_display_children(selected_country, selected_city):
    return u'{} is a city in {}'.format(
        selected_city,
        selected_country,
    )

if __name__ == '__main__':
    app.run_server(debug=True)

첫 번째 콜백은 첫 번째 dcc.RadioItems 컴포넌트에서 선택된 값을 기반으로 두 번째 dcc.RadioItems 컴포넌트에서 사용 가능한 옵션을 업데이트합니다.

두 번째 콜백은 options 속성이 변경될 때 초기 값을 설정합니다. 해당 options 배열의 첫 번째 값으로 설정합니다.

마지막 콜백은 각 컴포넌트의 선택된 value을 표시합니다. 만약 여러분이 국가 dcc.RadioItems 컴포넌트의 value을 변경한다면, 대시는 최종 콜백을 호출하기 전에 도시 구성 요소의 value가 업데이트될 때까지 기다릴 것입니다. 이렇게 하면  "America""Montréal"와 같이 일관되지 않은 상태로 콜백이 호출되는 것을 방지할 수 있습니다.

Dash App With State

경우에 따라 애플리케이션에 여러 개의 입력을 받는 폼(form)이 있을 수 있습니다. 이러한 상황에서는 입력 컴포넌트의 값을 읽되, 변경 직후가 아니라 사용자가 양식에 모든 정보를 입력한 후에만 읽어야 할 수 있습니다.

# -*- coding: utf-8 -*-
from dash import Dash, dcc, html
from dash.dependencies import Input, Output

external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]

app = Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div(
    [
        dcc.Input(id="input-1", type="text", value="Montréal"),
        dcc.Input(id="input-2", type="text", value="Canada"),
        html.Div(id="number-output"),
    ]
)

@app.callback(
    Output("number-output", "children"),
    Input("input-1", "value"),
    Input("input-2", "value"),
)
def update_output(input1, input2):
    return u'Input 1 is "{}" and Input 2 is "{}"'.format(input1, input2)

if __name__ == "__main__":
    app.run_server(debug=True)

이 예제에서는 Input에 설명된 속성이 변경될 때마다 콜백 함수가 실행됩니다. 위의 입력에 데이터를 입력하여 직접 사용해 보세요.

State를 사용하면 콜백을 실행하지 않고 추가 값을 전달할 수 있습니다. 다음은 위와 동일한 예시이지만 두 개의 dcc.Input 컴포넌트를 State로, 새 버튼 컴포넌트를 Input으로 추가한 예시입니다.

# -*- coding: utf-8 -*-
from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div(
    [
        dcc.Input(id='input-1-state', type='text', value='Montréal'),
        dcc.Input(id='input-2-state', type='text', value='Canada'),
        html.Button(id='submit-button-state', n_clicks=0, children='Submit'),
        html.Div(id='output-state'),
    ]
)

@app.callback(
    Output('output-state', 'children'),
    Input('submit-button-state', 'n_clicks'),
    State('input-1-state', 'value'),
    State('input-2-state', 'value'),
)
def update_output(n_clicks, input1, input2):
    return u'''
        The Button has been pressed {} times,
        Input 1 is "{}",
        and Input 2 is "{}"
    '''.format(
        n_clicks, input1, input2
    )

if __name__ == '__main__':
    app.run_server(debug=True)

이 예제에서는 dcc.Input 상자의 텍스트를 변경해도 콜백이 실행되지 않습니다. 버튼을 클릭하면 콜백이 실행됩니다. 콜백 함수 자체를 트리거하지 않더라도 dcc.Input 값의 현재 값은 여전히 콜백에 전달됩니다.

우리는 html.Button 컴포넌트의 n_clicks 속성을 수신하여 콜백을 트리거하고 있다는 점에 유의하세요. n_clicks는 컴포넌트가 클릭될 때마다 증가되는 속성입니다. 대시 HTML 컴포넌트(dash.html)의 모든 컴포넌트에서 사용할 수 있지만, 버튼에 가장 유용합니다.