(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)

費弗裡發表於2021-04-04

本文示例程式碼已上傳至我的Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes

1 簡介

   這是我的系列教程Python+Dash快速web應用開發的第十二期,在以前撰寫過的靜態部件篇(中)那期教程中,我們介紹過在Dash中建立靜態表格的方法。

  而在實際的使用中,我們很多時候在網頁中渲染的表格不僅僅是為了對資料進行展示,還需要更多互動能力,譬如按列排序動態修改表中數值等特性,以及對大型資料表快速渲染檢視能力,諸如此類眾多的互動功能在Dash自帶的dash_table中已經實現。

  而接下來的幾期,我們就將針對如何利用dash_table建立具有豐富互動功能的表格進行介紹,今天介紹的是dash_table的基礎使用方法。

(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖1

2 dash_table基礎使用

  作為Dash自帶的擴充庫,我們通過下列語句匯入dash_table

import dash_table

  接著像之前使用其他的Dash部件一樣,在定義layout時將dash_table.DataTable()物件置於我們定義的合適位置即可,可參考下面的例子配合pandasDataFrame來完成最簡單的表格的渲染。

  其中引數columns用於設定每一列對應的名稱與id屬性,data接受由資料框轉化而成的特殊格式資料,virtualization設定為True代表使用了虛擬化技術來加速網頁中大量表格行資料的渲染:

app1.py

import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table

import seaborn as sns

app = dash.Dash(__name__)

# 載入演示資料集
df = sns.load_dataset('iris')
# 建立行下標列
df.insert(loc=0, column='#', value=df.index)

app.layout = html.Div(
    dbc.Container(
        dash_table.DataTable(
            columns=[{'name': column, 'id': column} for column in df.columns],
            data=df.to_dict('records'),
            virtualization=True
        ),
        style={
            'margin-top': '100px'
        }
    )
)

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

  如果你對資料的展示完全沒要求,看個數就行,那上述的這套基礎的引數設定你就可以當成萬金油來使用,而如果你覺得dash_table.DataTable預設太醜了(大實話),那麼請繼續閱讀今天的教程。

(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖2

2.1 自定義表格基礎樣式

  針對DataTable所渲染出的表格的幾個基礎構成部分,我們可以使用到的用於修改表格樣式的引數有style_tablestyle_cellstyle_headerstyle_data等:

  • 使用style_table來自定義表格外層容器樣式

  引數style_table用於對整個表格最外層的容器樣式傳入css鍵值對進行修改,一般用來設定表格的高度、寬度、周圍留白或對齊等屬性:

app2.py

import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table

import seaborn as sns

app = dash.Dash(__name__)

# 載入演示資料集
df = sns.load_dataset('iris')
# 建立行下標列
df.insert(loc=0, column='#', value=df.index)

app.layout = html.Div(
    dbc.Container(
        [
            dash_table.DataTable(
                columns=[{'name': column, 'id': column} for column in df.columns],
                data=df.to_dict('records'),
                virtualization=True,
                style_table={
                    'height': '200px',
                    'margin-top': '100px'
                }
            ),
            html.Hr(),
            dash_table.DataTable(
                columns=[{'name': column, 'id': column} for column in df.columns],
                data=df.to_dict('records'),
                virtualization=True,
                style_table={
                    'height': '200px',
                    'margin-left': '80px',
                    'width': '300px'
                }
            ),
            html.Hr(),
            dash_table.DataTable(
                columns=[{'name': column, 'id': column} for column in df.columns],
                data=df.to_dict('records'),
                virtualization=True,
                style_table={
                    'height': '150px',
                    'width': '50%',
                    'margin-left': '50%'
                }
            )
        ],
        style={
            'background-color': '#bbdefb'
        }
    )
)

if __name__ == '__main__':
    app.run_server(debug=True)
(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖3
  • 使用style_cell、style_header與style_data定義單元格樣式

  不同於style_table,使用style_cell可以傳入css將樣式應用到所有單元格,而style_headerstyle_data則更加有針對性,可分別對標題單元格、資料單元格進行設定:

app3.py

import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table

import seaborn as sns

app = dash.Dash(__name__)

# 載入演示資料集
df = sns.load_dataset('iris')
# 建立行下標列
df.insert(loc=0, column='#', value=df.index)

app.layout = html.Div(
    dbc.Container(
        [
            dash_table.DataTable(
                columns=[{'name': column, 'id': column} for column in df.columns],
                data=df.to_dict('records'),
                virtualization=True,
                style_table={
                    'height': '300px'
                },
                style_cell={
                    'background-color': '#fff9c4',
                    'font-family': 'Times New Romer',
                    'text-align': 'center'
                }
            ),
            html.Hr(),
            dash_table.DataTable(
                columns=[{'name': column, 'id': column} for column in df.columns],
                data=df.to_dict('records'),
                virtualization=True,
                style_table={
                    'height': '300px'
                },
                style_header={
                    'background-color': '#b3e5fc',
                    'font-family': 'Times New Romer',
                    'font-weight': 'bold',
                    'font-size': '17px',
                    'text-align': 'left'
                },
                style_data={
                    'font-family': 'Times New Romer',
                    'text-align': 'left'
                }
            )
        ],
        style={
            'margin-top': '100px'
        }
    )
)

if __name__ == '__main__':
    app.run_server(debug=True)
(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖4
  • 條件樣式設定

  除了像上文所演示的那樣針對某一類表格構成元素進行整體樣式設定外,DataTable還為我們提供了條件樣式設定,比如我們想為特殊的幾列單獨設定樣式,或者為奇數下標與偶數下標行設定不同的樣式,就可以使用到這一特性。

  這在DataTable中我們可以利用style_header_conditionalstyle_data_conditional來傳入列表,列表中每個元素都可看做是帶有額外if鍵值對的css引數字典,而這個if鍵值對的值亦為一個字典,其接受的鍵值對種類豐富,我們今天先來介紹column_idrow_index,它們分別用來指定對應idheader與整行單元格。

  參考下面這個例子,我們分別特殊設定#列的表頭與奇數行的樣式:

app4.py

import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table

import seaborn as sns

app = dash.Dash(__name__)

# 載入演示資料集
df = sns.load_dataset('iris')
# 建立行下標列
df.insert(loc=0, column='#', value=df.index)

app.layout = html.Div(
    dbc.Container(
        [
            dash_table.DataTable(
                columns=[{'name': column, 'id': column} for column in df.columns],
                data=df.to_dict('records'),
                virtualization=True,
                style_table={
                    'height': '500px'
                },
                style_cell={
                    'font-family': 'Times New Romer',
                    'text-align': 'center'
                },
                style_header_conditional=[
                    {
                        'if': {
                            # 選定列id為#的列
                            'column_id': '#'
                        },
                        'font-weight': 'bold',
                        'font-size': '24px'
                    }
                ],
                style_data_conditional=[
                    {
                        'if': {
                            # 選中行下標為奇數的行
                            'row_index': 'odd'
                        },
                        'background-color': '#cfd8dc'
                    }
                ]
            )
        ],
        style={
            'margin-top': '100px'
        }
    )
)

if __name__ == '__main__':
    app.run_server(debug=True)
(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖5
  • 隱藏所有豎直框線

  設定引數style_as_list_view為True可以隱藏所有豎向的框線,app4設定之後的效果如下:

(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖6

3 動手製作一個資料入庫應用

  學習完今天的內容之後,我們來動手寫一個簡單的資料入庫應用,通過拖入本地csv檔案以及填寫入庫表名,來實現對上傳資料的預覽與資料庫匯入,後端會自動檢查使用者輸入的資料表名稱是否合法,並自動檢測上傳csv檔案的檔案編碼。

  下面就是該應用工作時的情景,其中因為test表在庫中已存在,所以會被檢測出不合法:

(資料科學學習手札115)Python+Dash快速web應用開發——互動表格篇(上)
圖7

  而當上傳的資料錶行數較多時,右下角會自動出現分頁部件,我們將在下一期中進行討論,完整程式碼如下:

app5.py

import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
import dash_table
import dash_uploader as du

import re
import os
import pandas as pd
from sqlalchemy import create_engine
import cchardet as chardet # 用於自動識別檔案編碼

postgres_url = 'postgresql://postgres:CUDLCUDL@localhost:5432/Dash'
engine = create_engine(postgres_url)

app = dash.Dash(__name__)

du.configure_upload(app, 'upload')

app.layout = html.Div(
    dbc.Container(
        [
            du.Upload(
                id='upload',
                filetypes=['csv'],
                text='點選或拖動檔案到此進行上傳!',
                text_completed='已完成上傳檔案:',
                cancel_button=True,
                pause_button=True),
            html.Hr(),
            dbc.Form(
                [
                    dbc.FormGroup(
                        [
                            dbc.Label("設定入庫表名", html_for="table-name"),
                            dbc.Input(
                                id='table-name',
                                autoComplete='off'
                            ),
                            dbc.FormText(
                                "表名只允許包含大小寫字母、下劃線或數字,且不能以數字開頭,同時請注意表名是否與庫中現有表重複!", color="secondary"
                            ),
                            dbc.FormFeedback(
                                "表名合法!", valid=True
                            ),
                            dbc.FormFeedback(
                                "表名不合法!",
                                valid=False,
                            ),
                        ]
                    ),
                    dbc.FormGroup(
                        [
                            dbc.Button('提交入庫', id='commit', outline=True)
                        ]
                    )
                ],
                style={
                    'background-color': 'rgba(224, 242, 241, 0.4)'
                }
            ),
            dbc.Spinner(
                [
                    html.P(id='commit-status-message', style={'color': 'red'}),
                    dbc.Label('預覽至多前10000行', html_for='uploaded-table'),
                    dash_table.DataTable(
                        id='uploaded-table',
                        style_table={
                            'height': '400px'
                        },
                        virtualization=True,
                        style_as_list_view=True,
                        style_cell={
                            'font-family': 'Times New Romer',
                            'text-align': 'center'
                        },
                        style_header={
                            'font-weight': 'bold'
                        },
                        style_data_conditional=[
                            {
                                'if': {
                                    # 選中行下標為奇數的行
                                    'row_index': 'odd'
                                },
                                'background-color': '#cfd8dc'
                            }
                        ]
                    )
                ]
            )
        ],
        style={
            'margin-top': '30px'
        }
    )
)


@app.callback(
    [Output('table-name', 'invalid'),
     Output('table-name', 'valid')],
    Input('table-name', 'value')
)
def check_table_name(value):
    ''''
    檢查表名是否合法
    '''
    if value:

        # 查詢庫中已存在非系統表名
        exists_table_names = (
            pd
                .read_sql('''SELECT tablename FROM pg_tables''', con=engine)
                .query('~(tablename.str.startswith("pg") or tablename.str.startswith("sql_"))')
        )

        if (re.findall('^[A-Za-z0-9_]+$', value)[0].__len__() == value.__len__()) \
                and not re.findall('^\d', value) \
                and value not in exists_table_names['tablename'].tolist():
            return False, True

        return True, False

    return dash.no_update


@app.callback(
    Output('commit-status-message', 'children'),
    Input('commit', 'n_clicks'),
    [State('table-name', 'valid'),
     State('table-name', 'value'),
     State('upload', 'isCompleted'),
     State('upload', 'fileNames'),
     State('upload', 'upload_id')]
)
def control_table_commit(n_clicks,
                         table_name_valid,
                         table_name,
                         isCompleted,
                         fileNames,
                         upload_id):
    '''
    控制已上傳表格的入庫
    '''
    if all([n_clicks, table_name_valid, table_name, isCompleted, fileNames, upload_id]):
        uploaded_df = pd.read_csv(os.path.join('upload', upload_id, fileNames[0]),
                                  encoding=chardet.detect(open(os.path.join('upload', upload_id, fileNames[0]),
                                                               'rb').read())['encoding'])

        uploaded_df.to_sql(table_name, con=engine)

        return '入庫成功!'

    return dash.no_update


@app.callback(
    [Output('uploaded-table', 'data'),
     Output('uploaded-table', 'columns')],
    Input('upload', 'isCompleted'),
    [State('upload', 'fileNames'),
     State('upload', 'upload_id')]
)
def render_table(isCompleted, fileNames, upload_id):
    '''
    控制預覽表格的渲染
    '''
    if isCompleted:
        uploaded_df = pd.read_csv(os.path.join('upload', upload_id, fileNames[0]),
                                  encoding=chardet.detect(open(os.path.join('upload', upload_id, fileNames[0]),
                                                               'rb').read())['encoding']).head(10000)

        uploaded_df.insert(0, '#', range(uploaded_df.shape[0]))

        return uploaded_df.to_dict('record'), [{'name': column, 'id': column} for column in uploaded_df.columns]

    return dash.no_update


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

  以上就是本文的全部內容,歡迎在評論區與我進行討論~

相關文章