本文示例程式碼已上傳至我的
Github
倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
這是我的系列教程Python+Dash快速web應用開發的第十八期,通過前面十七期的內容,如果你有用心學習的話,那麼恭喜你已經具備使用Dash
編寫常規web應用的能力了。
而在使用Dash
開發web應用時,頁面內容和功能邏輯簡單倒還好,一旦你的功能內容開始複雜化系統化起來,那麼像過往文章示例中簡單一個app.py
存放所有功能程式碼就不適用了。
而在今天的教程中,我就將為大家介紹我在日常使用過程中總結出的一套針對Dash
專案的前後端分離的專案結構基礎正規化,並以搭建全國七普部分資料視覺化看板為例,供大家參考借鑑,從而更有條理的編寫和管理Dash
應用專案。
2 Dash專案結構基礎正規化
2.1 總體結構一覽
開門見山,我們直接先來一覽今天要介紹的Dash
基礎專案結構:
+ dash_demo_project/
+ assets/
+ css/
+ img/
+ js/
• favicon.ico
+ callbacks/
+ models/
+ views/
• app.py
• server.py
在不考慮外部引數匯入、使用者登陸驗證、應用部署等額外配置檔案及功能內容的前提下,上面的結構就可以滿足常規Dash
應用的需求了。
下面我們基於和鯨上獲取到的第七次全國人口普查公開資料集,以搭建下面這個簡單的資料視覺化看板為例,介紹上述各部分的實際功能意義(完整專案原始碼見文章開頭連結)。
2.2 各部分結構介紹
2.2.1 再談assets
在頁面佈局篇中我們提到過assets
目錄,它是官方推薦的用於存放我們的Dash
應用所依賴靜態資原始檔的目錄,如依賴的css
、js
、favicon.ico
、各種圖片及字型等靜態資源,在本文的視覺化看板案例中,assets
目錄資源放置情況如下:
+ assets/
+ css/
• bootstrap.min.css
• custom.css
+ img/
• wxgzh.png
• zsxq.png
+ js/
• favicon.ico
其中img
目錄下存放的是首頁的兩張二維碼圖片,在Dash
中可以配合Img()
與get_asset_url()
來獲取assets
目錄下指定檔案路徑並渲染:
html.Img(src=app.get_asset_url('img/zsxq.png'), style={'width': '100%'})
而css
目錄下則放置了dash_bootstrap-components
所依賴的css
檔案,而custom.css
則是我自己編寫的一些用於樣式美化的css
程式碼:
.nav-link.active {
background-color: #4fc3f7!important;
}
#index-desc > * {
font-size: 26px;
}
.table td, .table th {
text-align: center;
}
直接放置於assets
根目錄下的favicon.ico
則用來替換Dash
預設的網頁圖示:
你可以根據自己Dash
專案的實際需求靈活變通,譬如需要用到echarts
就可以在js
目錄下放置echarts.min.js
檔案。
2.2.2 在server.py中例項化配置Dash物件
跟以往的例子不同,在嚴謹的Dash
工程下,推薦構建單獨的server.py
檔案來完成對Dash
物件的例項化配置等工作,在今天的視覺化看板案例中server.py
比較簡單,內容如下:
import dash
app = dash.Dash(
__name__,
suppress_callback_exceptions=True
)
# 設定網頁title
app.title = '七普部分資料看板'
server = app.server
2.2.3 在app.py中編寫前端骨架與路由
如果你的Dash
專案非常簡單,那麼from server import app
之後,就可以像往常一樣在app.py
中組織你的前端與回撥部分內容。
但如果你的Dash
專案功能較為複雜,亦或是url聯結的頁面較多時,就可以只在app.py
中編寫前端layout
骨架,包含了必要的Location()
部件、保持不變的前端部分以及由url
變化所觸發的頁面內容容器,譬如今天的視覺化看板中左側邊欄部分以及Location()
監聽部件:
app.layout = html.Div(
[
# 監聽url變化
dcc.Location(id='url'),
html.Div(
[
# 標題區域
html.Div(
html.H3(
'七普部分資料看板',
style={
'marginTop': '20px',
'fontFamily': 'SimSun',
'fontWeight': 'bold'
}
),
style={
'textAlign': 'center',
'margin': '0 10px 0 10px',
'borderBottom': '2px solid black'
}
),
# 子頁面區域
html.Hr(),
dbc.Nav(
[
dbc.NavLink('首頁', href='/', active="exact"),
dbc.NavLink('年齡結構', href='/age', active="exact"),
dbc.NavLink('性別結構', href='/sex', active="exact"),
dbc.NavLink('六普vs七普', href='/statistics', active="exact"),
],
vertical=True,
pills=True
)
],
style={
'flex': 'none',
'width': '300px',
'backgroundColor': '#fafafa'
}
),
html.Div(
id='page-content',
style={
'flex': 'auto'
}
)
],
style={
'width': '100vw',
'height': '100vh',
'display': 'flex'
}
)
同樣地,也推薦將監聽url變化從而渲染不同頁面的路由回撥一併寫在app.py
中,方便後續的管理與升級:
# 路由總控
@app.callback(
Output('page-content', 'children'),
Input('url', 'pathname')
)
def render_page_content(pathname):
if pathname == '/':
return index_page
elif pathname == '/age':
return age_page
elif pathname == '/sex':
return sex_page
elif pathname == '/statistics':
return statistics_page
return html.H1('您訪問的頁面不存在!')
2.2.4 在views子模組中構建多頁面前端內容
在上一小節的路由回撥中你可能會好奇不同url下的返回值index_page
、age_page
等都是什麼,這些都構建在子模組views
下:
+ views/
• age.py
• index.py
• sex.py
• statistics.py
• __init__.py
譬如其中之一的age.py
內容如下:
import dash_html_components as html
import dash_core_components as dcc
import dash_bootstrap_components as dbc
import pandas as pd
import plotly.express as px
from models.age import Age
age_data = (
pd.DataFrame(Age.fetch_all()).rename(columns={
'region': '地區',
'prop_0_to_14': '0到14歲人口占比',
'prop_15_59': '15到59歲人口占比',
'prop_60_above': '60歲以上人口占比',
'prop_65_above': '65歲以上人口占比'
})
)
fig = px.bar(age_data.melt(id_vars=['地區'],
value_vars=['0到14歲人口占比', '15到59歲人口占比', '60歲以上人口占比'],
var_name='年齡段',
value_name='佔比(%)'),
y="地區", x="佔比(%)", color="年齡段", title="七普各地區人口年齡結構",
color_discrete_map={
'0到14歲人口占比': '#0868ac',
'15到59歲人口占比': '#43a2ca',
'60歲以上人口占比': '#a8ddb5'
},
orientation='h')
fig.update_layout(
font=dict(
family="Times New Roman, SimSun"
)
)
fig.update_layout(xaxis_range=[0, 100])
fig.update_layout(
margin=dict(t=50, b=10)
)
age_page = html.Div(
[
html.Div(
dbc.Table.from_dataframe(age_data, striped=True),
style={
'overflowY': 'auto',
'flex': '1'
}
),
html.Div(
dcc.Graph(figure=fig, style={'height': '100%'}),
style={
'flex': '1',
'height': '100%'
}
)
],
style={
'display': 'flex',
'height': '100%'
}
)
通過這種方式針對不同頁面構建相應的前端物件,從而在app.py
中按照下列方式匯入就可以使用了:
from views.index import index_page
from views.age import age_page
from views.sex import sex_page
from views.statistics import statistics_page
2.2.5 在callbacks子模組中構建多頁面後端邏輯
當你在views
下構建的頁面內容中涉及到回撥互動的功能時,我推薦將對應的後端回撥邏輯拆分到callbacks
子模組下同名檔案中,這樣非常便於編寫與維護。
同時一定要記住在views
下對應的前端子模組中,一定要匯入callbacks
中對應的回撥子模組內部的至少一個物件,否則Dash
在打包應用時是掃描不到相應的回撥函式內容進行編譯的,進而會導致應用啟動時回撥無效,譬如在views/statistics.py
中我們就執行了from callbacks.statistics import statistics_data
。
2.2.6 在models子模組下定義資料模型
前面說的很多內容都關乎Dash
應用的構建,而當你的Dash
應用依賴外部資料時,推薦的方式是類似flask
專案那樣構建子模組models
來定義資料模型,實現與資料庫的關聯。
而我們今天的視覺化看板案例中就配合整合資料庫篇介紹的peewee
相關知識,分別定義了資料模型對應了七普中的年齡結構
、性別結構
以及六普七普對比
資料表,並在views
、callbacks
等涉及的子模組中匯入並呼叫,以年齡結構models/age.py
為例:
from peewee import SqliteDatabase, Model
from peewee import CharField, FloatField
db = SqliteDatabase('models/age.db')
class Age(Model):
# 地區,唯一
region = CharField(unique=True)
# 0-14歲佔比
prop_0_to_14 = FloatField()
# 15-59歲佔比
prop_15_59 = FloatField()
# 60歲及以上佔比
prop_60_above = FloatField()
# 65歲及以上佔比
prop_65_above = FloatField()
class Meta:
database = db
primary_key = False # 禁止自動生成唯一id列
@classmethod
def fetch_all(cls):
return list(cls.select().dicts())
而本文案例中涉及到的資料視覺化內容均由plotly
及plotly.express
實現,關於這部分內容我會在之後的進階教程中加以概括。
本文完整專案案例原始碼+附件
你可以在文章開頭連結頁面檢視和下載。
下期我將帶大家學習如何在Linux
、Windows
等系統中正式部署Dash
應用,敬請期待。
以上就是本文的全部內容,歡迎在評論區發表你的意見和想法。