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

費弗裡發表於2021-04-10

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

1 簡介

   這是我的系列教程Python+Dash快速web應用開發的第十三期,在上一期中,我們一起認識了Dash自帶的互動式表格元件dash_table,並學會了如何自定義表格中不同部分的樣式。

  而今天的教程,我們將繼續深入認識dash_table的更多互動方面的功能,學習如何為渲染出的表格分頁,並新增動態內容修改等互動功能。

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

2 dash_table的基礎互動能力

  dash_table的核心功能是賦予使用者與圖表進行快捷互動的能力,下面我們來學習其基礎常用的一些互動功能:

2.1 分頁翻頁

  當我們要展示的資料行數較多時,在網頁中渲染可以選擇分頁,這在dash_table中實現起來比較方便,根據資料傳遞方式的不同,可以分為前端分頁後端分頁

2.1.1 前端分頁

  前端分頁顧名思義,就是在我們訪問Dash應用時,表格內所有頁面的資料一次性載入完成,適合資料量不大的情況,將資料儲存壓力轉移到瀏覽器端。

  通過引數page_size設定每頁要顯示的記錄行數,Dash會自動幫我們分好頁,並配上翻頁部件:

app1.py

import dash
import dash_bootstrap_components as dbc
import dash_table

import seaborn as sns

df = sns.load_dataset('tips')
df.insert(0, '#', df.index)

app = dash.Dash(__name__)

app.layout = dbc.Container(
    [
        dash_table.DataTable(
            id='dash-table',
            data=df.to_dict('records'),
            columns=[
                {'name': column, 'id': column}
                for column in df.columns
            ],
            page_size=15,  # 設定單頁顯示15行記錄行數
            style_header={
                'font-family': 'Times New Romer',
                'font-weight': 'bold',
                'text-align': 'center'
            },
            style_data={
                'font-family': 'Times New Romer',
                'text-align': 'center'
            }
        )
    ],
    style={
        'margin-top': '50px'
    }
)

if __name__ == '__main__':
    app.run_server(debug=True)
(資料科學學習手札116)Python+Dash快速web應用開發——互動表格篇(中)
圖2

2.1.2 後端分頁

  雖然前端分頁簡單易用,但當我們的資料很大時,強行使用前端分頁會給網路傳輸瀏覽器端帶來不小的延遲和記憶體壓力,嚴重影響使用者體驗,因此Dash貼心地為我們準備了後端分頁方式。

  這時首先我們得為DataTable設定引數page_action='custom',這是使用後端分頁的先決條件,接下來我們需要認識一些新的引數:

  page_current,int型,對應當前翻到的頁碼;

  page_count,int型,對應顯示的總頁數;

  我們在使用後端分頁時,實際上就是通過使用者當前翻到的頁碼,以及設定的page_size,來動態地在翻頁後載入對應批次的資料,並控制顯示的總頁數,參考下面這個簡單的例子:

app2.py

import dash
import dash_bootstrap_components as dbc
import dash_table
from dash.dependencies import Input, Output

import seaborn as sns
import pandas as pd
from tqdm import tqdm

# 壓力測試
df = pd.concat([sns.load_dataset('tips') for _ in tqdm(range(1000))], ignore_index=True)
df.insert(0, '#', df.index)

app = dash.Dash(__name__)

app.layout = dbc.Container(
    [
        dbc.Spinner(
            dash_table.DataTable(
                id='dash-table',
                columns=[
                    {'name': column, 'id': column}
                    for column in df.columns
                ],
                page_size=15,  # 設定單頁顯示15行記錄行數
                page_action='custom',
                page_current=0,
                style_header={
                    'font-family': 'Times New Romer',
                    'font-weight': 'bold',
                    'text-align': 'center'
                },
                style_data={
                    'font-family': 'Times New Romer',
                    'text-align': 'center'
                }
            )
        )
    ],
    style={
        'margin-top': '50px'
    }
)


@app.callback(
    [Output('dash-table', 'data'),
     Output('dash-table', 'page_count')],
    [Input('dash-table', 'page_current'),
     Input('dash-table', 'page_size')]
)
def refresh_page_data(page_current, page_size):
    return df.iloc[page_current * page_size:(page_current + 1) * page_size].to_dict('records'), 1 + df.shape[
        0] // page_size


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

  可以看到,即使我們完整的資料集被我concat到24萬行,載入應用以及網頁內翻頁時依然輕鬆自如毫無壓力,在實際應用中你還可以將翻頁部分改成受到LIMITOFFSET控制的資料庫查詢過程,使得應用執行的更加快速高效:

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

2.2 對單元格內容進行編輯

  講完了分頁翻頁,接下來我們來學習dash_table中更加強大的功能——單元格內容編輯。

  一個現代化的web應用當然不能侷限於僅僅檢視資料這麼簡單,Dash同樣賦予了我們雙擊資料表單元格進行資料編輯的能力,首先得設定引數editable=True,即開啟表格編輯模式,接下來就可以對資料區域單元格進行任意的雙擊選中編輯。

  不過Dash預設的單元格被選中的樣式忒醜了(是粉色的你敢信),因此我們可以利用下面的引數設定方式來自定義美化:

style_data_conditional=[
                {
                    # 對選中狀態下的單元格進行自定義樣式
                    "if": {"state": "selected"},
                    "background-color": "#b3e5fc",
                    "border": "none"
                },
            ]

  來看一個形象的例子,我們對前端分頁方式渲染出的表格進行隨意的修改,並在下方對利用pandascompare比較出的資料框之間的差異結果進行列印:

app3.py

import dash
import dash_html_components as html
import dash_core_components as dcc
import dash_bootstrap_components as dbc
import dash_table
from dash.dependencies import Input, Output

import seaborn as sns
import pandas as pd

df = sns.load_dataset('tips')
df.insert(0, '#', df.index)

app = dash.Dash(__name__)

app.layout = dbc.Container(
    [
        dash_table.DataTable(
            id='dash-table',
            data=df.to_dict('records'),
            columns=[
                {'name': column, 'id': column}
                for column in df.columns
            ],
            fixed_rows={'headers': True},
            page_size=15,
            editable=True,
            style_header={
                'font-family': 'Times New Romer',
                'font-weight': 'bold',
                'text-align': 'center'
            },
            style_data={
                'font-family': 'Times New Romer',
                'text-align': 'center'
            },
            style_data_conditional=[
                {
                    # 對選中狀態下的單元格進行自定義樣式
                    "if": {"state": "selected"},
                    "background-color": "#b3e5fc",
                    "border": "none"
                },
            ]
        ),
        html.H4('與原表格內容比較:', style={'margin-top': '50px'}),
        dcc.Markdown(
            '無差別',
            id='markdown',
            dangerously_allow_html=True
        )
    ],
    style={
        'margin-top': '50px'
    }
)


@app.callback(
    Output('markdown', 'children'),
    Input('dash-table', 'data'),
    prevent_initial_call=True
)
def compare_difference(dash_table_data):
    print(pd.DataFrame(dash_table_data))

    return df.compare(pd.DataFrame(dash_table_data)).to_html()


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

  可以看到,我們成功地對指定單元格元素進行了修改。

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

3 開發資料庫內容線上更新工具

  在學習完今天的內容之後,我們就可以開發一個簡單的,可線上自由修改並同步變動到資料庫的小工具,這裡我們以MySQL資料庫為例,對示例表進行修改和更新:

  首先我們利用下列程式碼向示例資料庫中新建表格tips

from sqlalchemy import create_engine
import seaborn as sns

df = sns.load_dataset('tips')
df.insert(0, '#', df.index)

engine = create_engine('mysql+pymysql://root:mysql@localhost/DASH')

df.to_sql('tips', con=engine, if_exists='replace', index=False)
(資料科學學習手札116)Python+Dash快速web應用開發——互動表格篇(中)
圖5
  

  接下來我們就以建立好的tips表為例,開發一個Dash應用,進行資料的修改和更新到資料庫:

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

  效果非常的不錯,你可以在我這個簡單示例的基礎上,擴充更多新功能,也可以採取後端分頁+條件修改的方式來應對大型資料表的修改,全部程式碼如下:

app4.py

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State

from sqlalchemy import create_engine
import pandas as pd

engine = create_engine('mysql+pymysql://root:mysql@localhost/DASH')

app = dash.Dash(__name__)

app.layout = dbc.Container(
    [
        dbc.Row(
            [
                dbc.Col(dbc.Button('更新資料表', id='refresh-tables', style={'width': '100%'}), width=2),
                dbc.Col(dcc.Dropdown(id='table-select', style={'width': '100%'}), width=2)
            ]
        ),
        html.Hr(),
        dash_table.DataTable(
            id='dash-table',
            editable=True,
            page_size=15,
            style_header={
                'font-family': 'Times New Romer',
                'font-weight': 'bold',
                'text-align': 'center'
            },
            style_data={
                'font-family': 'Times New Romer',
                'text-align': 'center'
            },
            style_data_conditional=[
                {
                    # 對選中狀態下的單元格進行自定義樣式
                    "if": {"state": "selected"},
                    "background-color": "#b3e5fc",
                    "border": "none"
                },
            ]
        ),
        dbc.Button('同步變動到資料庫', id='update-tables', style={'display': 'none'}),
        html.P(id='message')
    ],
    style={
        'margin-top': '50px'
    }
)


@app.callback(
    Output('table-select', 'options'),
    Input('refresh-tables', 'n_clicks')
)
def refresh_tables(n_clicks):
    if n_clicks:
        return [
            {
                'label': table,
                'value': table
            }
            for table in pd.read_sql_query('SHOW TABLES', con=engine)['Tables_in_dash']
        ]

    return dash.no_update


@app.callback(
    [Output('dash-table', 'data'),
     Output('dash-table', 'columns'),
     Output('update-tables', 'style')],
    Input('table-select', 'value')
)
def render_dash_table(value):
    if value:
        df = pd.read_sql_table(value, con=engine)

        return df.to_dict('records'), [
            {'name': column, 'id': column}
            for column in df.columns
        ], {'margin-top': '25px'}

    else:
        return [], [], {'display': 'none'}


@app.callback(
    [Output('message', 'children'),
     Output('message', 'style')],
    Input('update-tables', 'n_clicks'),
    [State('dash-table', 'data'),
     State('table-select', 'value')]
)
def update_to_database(n_clicks, data, value):

    if n_clicks:

        try:
            pd.DataFrame(data).to_sql(value, con=engine, if_exists='replace', index=False)

            return '更新成功!', {'color': 'green'}
        except Exception as e:
            return f'更新失敗!{e}', {'color': 'red'}

    return dash.no_update

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

  以上就是本文的全部內容,歡迎在評論區發表你的意見與觀點。

相關文章