Posted: Tuesday January 25th, 2022/Under: News, IT technique/By: Nghia Nguyen Trung Show Giới thiệu về DashDash là một thư viện mã nguồn mở được phát hành theo giấy phép MIT. Được viết trên Plotly.js và React.js, Dash lý tưởng cho việc xây dựng và triển khai các ứng dụng dữ liệu với UI (giao diện người dùng) tùy chỉnh. Dash đủ đơn giản để bạn có thể liên kết UI với code của mình trong vòng chưa đầy 10 phút. Ứng dụng Dash được render lên trình duyệt web, vì vậy nó có thể chạy trên đa nền tảng và cả thiết bị di động. Cài đặt DashYêu cầu máy cài sẵn Python 3. pip install dash Với lệnh trên thì ngoài dash, pip cũng sẽ cài đặt thư viện hỗ trợ vẽ đồ thị đó là Plotly.py. Và cuối cùng chúng ta cần cài đặt thư viện Pandas bằng lệnh: pip install pandas Về PandasPandas là một thư viện mã nguồn mở được phát hành theo giấy phép BSD. Pandas cung cấp các cấu trúc dữ liệu hiệu suất cao, dễ sử dụng và các công cụ phân tích dữ liệu cho ngôn ngữ lập trình Python. Pandas cung cấp 2 cấu trúc dữ liệu chính là DataFrame và Series. DataFrame là một cấu trúc dữ liệu 2 chiều có thể lưu trữ dữ liệu thuộc các loại khác nhau (bao gồm ký tự, số nguyên, giá trị dấu phẩy động, dữ liệu phân loại và hơn thế nữa) trong các cột. Mỗi cột trong DataFrame là một Series. Có ba quy ước phổ biến để lưu trữ dữ liệu dạng cột:
Dash LayoutỨng dụng Dash bao gồm hai phần. Phần đầu tiên là “layout” của ứng dụng và nó mô tả ứng dụng đó trông như thế nào. Phần thứ hai mô tả khả năng tương tác của ứng dụng, nó là “callbacks”. “Layout” là 1 cây tập hợp các “components“. Dash cung cấp rất nhiều loại component: Dash HTML Components, Dash Core Components, Dash DataTable, Dash DAQ, Dash Bootstrap Components,… Trong khuôn khổ của bài viết chúng ta sẽ tìm hiểu Dash HTML Components và Dash Core Components. Dash HTML ComponentsLà fuction cung cấp các component kiểu HTML, dùng để định nghĩa các HTML tag cho layout. Để dùng Dash HTML Components chúng ta cần import vào file .py như sau: Ví dụ: html_demo.py import dash from dash import html app = dash.Dash(__name__) app.layout = html.Div(children=[ html.H1(children='Hello Dash'), ]) if __name__ == '__main__': app.run_server(debug=True)
Cũng giống như các thẻ HTML tag, chúng ta hoàn toàn có thể thay đổi style của html_component bằng property “style“. Ví dụ: Đoạn mã trên được hiển thị dưới dạng Có một số khác biệt quan trọng giữa Dash HTML Components và các thuộc tính HTML:
Ngoài ra thay vì dùng style trực tiếp chúng ta có thể dùng file CSS để định nghĩa style cho layout, chi tiết tham khảo: https://dash.plotly.com/external-resources Bạn có thể xem tất cả các component có sẵn trong Dash HTML Components Gallery: https://dash.plotly.com/dash-html-components Dash Core ComponentsBao gồm một tập hợp các thành phần cấp cao hơn như dropdown, checkbox, radio, graph, v.v. Bạn có thể xem tất cả các component có sẵn trong Dash Core Components Gallery: https://dash.plotly.com/dash-core-components Để dùng Dash Core Components chúng ta cần import vào file .py như sau: Trong các core component thì “Graph” là component quan trọng đối với Trực quan hóa dữ liệu. “Graph” hiển thị trực quan hóa dữ liệu trên trình duyệt bằng cách sử dụng thư viện javascript vẽ đồ thị mã nguồn mở Plotly.js. Plotly.js hỗ trợ hơn 35 loại biểu đồ và hiển thị biểu đồ ở cả vector-quality SVG và high-performance WebGL. Một lưu ý nhỏ ở đây là Plotly.js chỉ dùng để render lên trình duyệt (do Dash thực hiện) còn khi code chúng ta sẽ dùng thư viện Plotly.py (được cung cấp sẵn khi cài đặt Dash) chứ không code trực tiếp bằng javascript. Để biết cách sử dụng “Graph” component chúng ta hãy đến với ví dụ hiển thị data csv lên trình duyệt dưới dạng đồ thị đường gấp khúc: csv/graph_sample.csv DateTime,DATA 1,DATA 2,DATA 3,DATA 4 20211220 101010.000,30,100,124,197 20211220 101010.010,40,110,134,65 20211220 101010.020,50,140,214,149 20211220 101010.030,60,150,169,-98 20211220 101010.040,70,160,204,-173 20211220 101010.050,80,170,164,-108 20211220 101010.060,90,180,148,150 20211220 101010.070,100,190,180,92 20211220 101010.080,110,200,268,94 20211220 101010.090,120,210,164,-139 20211220 101010.100,130,220,254,-132 Đầu tiên chúng ta cần dùng pandas để load file csv df = pd.read_csv('csv/graph_sample.csv') In biến df ra console xem thử cấu trúc của nó print(df) Tới đây bạn có thấy hơi quen quen không? Chính xác, nó là Dữ liệu dạng rộng mà chúng ta đã đề cập ở phần tìm hiểu Pandas ở trên! Bước tiếp theo chúng ta chuyển dữ liệu của cột DateTime từ string thành datetime để chart của chúng ta hiển thị chính xác ngày và giờ của dữ liệu df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y%m%d %H:%M:%S.%f') Bây giờ chúng ta tạo một line figure bằng plotly express line_fig = px.line(df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4']) Truyền figure vào Graph component app.layout = html.Div(children=[ dcc.Graph(id='graph', figure=line_fig) ]) Code hoàn chỉnh import dash import pandas as pd import plotly.express as px from dash import dcc from dash import html app = dash.Dash(__name__) df = pd.read_csv('csv/graph_sample.csv') print(df) df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y%m%d %H:%M:%S.%f') line_fig = px.line(df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4']) app.layout = html.Div(children=[ dcc.Graph(id='graph', figure=line_fig) ]) if __name__ == '__main__': app.run_server(debug=True) Trong terminal chạy lệnh: python graph_demo.py Sau đó truy cập http://127.0.0.1:8050/ để xem kết quả Trong ví dụ trên:
Dash CallbacksCallback functions: các hàm được Dash tự động gọi bất cứ khi nào thuộc tính của input component thay đổi, để cập nhật một số thuộc tính trong component khác (output). Để hiểu về Callbacks chúng ta hãy đến với ví dụ về filter dữ liệu theo ngày, với input lấy từ component dcc.DatePickerRange: DateTime,DATA 1,DATA 2,DATA 3,DATA 4 20211219 101010.010,10,200,178,90 20211219 111010.020,20,150,134,25 20211219 121010.030,5,130,210,11 20211219 131010.040,15,110,100,-97 20211219 141010.050,60,150,143,-17 20211219 151010.060,30,140,132,30 20211219 161010.070,20,180,167,45 20211219 171010.080,16,120,240,123 20211219 181010.090,75,190,153,40 20211219 191010.100,90,250,162,-10 20211220 001010.000,68,142,156,1 20211220 011010.010,40,110,134,65 20211220 021010.020,50,140,214,149 20211220 031010.030,60,150,169,-98 20211220 041010.040,70,160,204,-173 20211220 051010.050,80,170,164,-108 20211220 061010.060,90,180,148,150 20211220 071010.070,100,190,180,92 20211220 081010.080,110,200,268,94 20211220 091010.090,120,210,164,-139 20211220 101010.100,130,220,254,-132 20211221 001010.000,10,90,142,30 20211221 011010.010,30,100,162,55 20211221 021010.020,80,120,180,20 20211221 031010.030,70,110,176,-10 20211221 041010.040,50,130,194,-90 20211221 051010.050,60,140,202,-120 20211221 061010.060,90,150,164,100 20211221 071010.070,120,160,197,132 20211221 081010.080,110,170,186,40 20211221 091010.090,130,210,182,-130 20211221 101010.100,120,230,210,-100 callbacks_demo.py from datetime import datetime, timedelta import dash import pandas as pd import plotly.express as px from dash import dcc, Output, Input from dash import html app = dash.Dash(__name__) df = pd.read_csv('csv/callbacks_sample.csv') df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y%m%d %H:%M:%S.%f') init_start_date = df['DateTime'].min().strftime('%Y-%m-%d') init_end_date = df['DateTime'].max().strftime('%Y-%m-%d') app.layout = html.Div(children=[ dcc.DatePickerRange( id='date-picker-range', start_date=init_start_date, end_date=init_end_date, minimum_nights=0, display_format='YYYY/MM/DD' ), dcc.Graph(id='scatter-graph'), ]) @app.callback( Output('scatter-graph', 'figure'), Input('date-picker-range', 'start_date'), Input('date-picker-range', 'end_date') ) def update_figure(start_date, end_date): if start_date is not None and end_date is not None: start_date = datetime.fromisoformat(start_date) end_date = datetime.fromisoformat(end_date) + timedelta(days=1) filtered_df = df[(start_date <= df['DateTime']) & (df['DateTime'] <= end_date)] scatter_fig = px.scatter(filtered_df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4']) return scatter_fig if __name__ == '__main__': app.run_server(debug=True) Trong Dash, các input và output của ứng dụng của chúng ta chỉ đơn giản là các property của một component cụ thể. Trong ví dụ này, input của chúng ta là property “start_date” và “end_date” của component có ID “date-picker-range“. Output của chúng ta là property “figure” của component có ID “scatter-graph“. Bất cứ khi nào input property thay đổi, function mà có khai báo decorator @callback sẽ được gọi tự động. Dash cung cấp cho callback function này giá trị mới của input property làm argument (trong ví dụ trên function update_figurecó 2 argument là start_date, end_date), và Dash cập nhật property của output component với bất kỳ giá trị nào được function trả về (trong ví dụ trên function update_figuretrả về scatter_fig). Trong terminal chạy lệnh Sau khi thay đổi end_date Tối ưu hóa và thêm chức năngỞ phần này chúng ta lấy code ở phần Callbacks để tối ưu hóa và thêm chức năng cho nó. Đọc n DATAHiện tại chúng ta đang set cứng số lượng data đầu vào là 4. scatter_fig = px.scatter(filtered_df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4']) Giả sử chúng ta có số lượng data bất kỳ DATA 1, DATA 2,…, DATA n thì với đoạn code trên chúng ta chỉ có thể đọc và hiển thị được 4 data mà thôi. # get first columns name for x-axis x_col_name = df.columns[0] # get list column name except first column for y-axis y_col_name_list = df.columns[1:] filtered_df = df[(start_date <= df[x_col_name]) & (df[x_col_name] <= end_date)] scatter_fig = px.scatter(filtered_df, x=x_col_name, y=y_col_name_list) Đọc config từ header của CSVXét header của CSV như sau DateTime(yyyyMMdd HH:mm:ss.fff),DATA 1(minFilter=20;maxFilter=100),DATA 2(maxFilter=140),DATA 3,DATA 4,DATA 5 Chúng ta sẽ thêm chức năng đọc config từ header trên:
Đầu tiên thêm file utils.py chứa các function common import re _format_convertor = ( ('yyyy', '%Y'), ('yyy', '%Y'), ('yy', '%y'), ('y', '%y'), ('MMMM', '%B'), ('MMM', '%b'), ('MM', '%m'), ('M', '%m'), ('dddd', '%A'), ('ddd', '%a'), ('dd', '%d'), ('d', '%d'), ('HH', '%H'), ('H', '%H'), ('hh', '%I'), ('h', '%I'), ('mm', '%M'), ('m', '%M'), ('ss', '%S'), ('s', '%S'), ('tt', '%p'), ('t', '%p'), ('fff', '%f'), ('zzz', '%z'), ('zz', '%z'), ('z', '%z'), ) def convert_py_datetime_format(in_format): out_format = '' while in_format: if in_format[0] == "'": apos = in_format.find("'", 1) if apos == -1: apos = len(in_format) out_format += in_format[1:apos].replace('%', '%%') in_format = in_format[apos + 1:] elif in_format[0] == '\\': out_format += in_format[1:2].replace('%', '%%') in_format = in_format[2:] else: for intok, outtok in _format_convertor: if in_format.startswith(intok): out_format += outtok in_format = in_format[len(intok):] break else: out_format += in_format[0].replace('%', '%%') in_format = in_format[1:] return out_format def extract_csv_col_config(col_name: str): try: found = re.search('\\((.*)\\)', col_name) col_name = col_name.replace(found.group(0), '') config_string = found.group(1) config_list = config_string.split(';') configs = [] for config in config_list: key_value_list = config.split('=') key = key_value_list[0] value = key_value_list[1] if len(key_value_list) > 1 else None configs.append((key, value)) except AttributeError: configs = [] return col_name, configs Ở đoạn code trên:
Tiếp theo thêm function process_csv_variable vào app.py from datetime import datetime, timedelta import dash import numpy as np import pandas as pd import plotly.express as px from dash import dcc, Output, Input from dash import html from utils import extract_csv_col_config, convert_py_datetime_format def process_csv_variable(df_param): # process x-axis csv variable old_x_col_name = df_param.columns[0] new_x_col_name, configs = extract_csv_col_config(old_x_col_name) datetime_format = configs[0][0] df_param = df_param.rename(columns={old_x_col_name: new_x_col_name}) df_param[new_x_col_name] = pd.to_datetime(df_param[new_x_col_name], format=convert_py_datetime_format(datetime_format)) # process y-axis csv variable y_col_name_list = df_param.columns[1:] for old_y_col_name in y_col_name_list: new_y_col_name, configs = extract_csv_col_config(old_y_col_name) df_param = df_param.rename(columns={old_y_col_name: new_y_col_name}) for config, value in configs: if config == 'minFilter': df_param.loc[df_param[new_y_col_name] < int(value), new_y_col_name] = np.nan elif config == 'maxFilter': df_param.loc[df_param[new_y_col_name] > int(value), new_y_col_name] = np.nan return df_param app = dash.Dash(__name__) app.layout = html.Div(id='container', children=[ dcc.DatePickerRange( id='date-picker-range', minimum_nights=0, display_format='YYYY/MM/DD' ), dcc.Graph(id='scatter-graph'), ]) @app.callback( Output('date-picker-range', 'start_date'), Output('date-picker-range', 'end_date'), Input('container', 'id') ) def update_date_picker(id): df = pd.read_csv('csv/app_sample.csv') df = process_csv_variable(df) x_col_name = df.columns[0] init_start_date = df[x_col_name].min().strftime('%Y-%m-%d') init_end_date = df[x_col_name].max().strftime('%Y-%m-%d') return init_start_date, init_end_date @app.callback( Output('scatter-graph', 'figure'), Input('date-picker-range', 'start_date'), Input('date-picker-range', 'end_date') ) def update_figure(start_date, end_date): df = pd.read_csv('csv/app_sample.csv') df = process_csv_variable(df) if start_date is not None and end_date is not None: start_date = datetime.fromisoformat(start_date) end_date = datetime.fromisoformat(end_date) + timedelta(days=1) # get first columns name for x-axis x_col_name = df.columns[0] # get list column name except first column for y-axis y_col_name_list = df.columns[1:] filtered_df = df[(start_date <= df[x_col_name]) & (df[x_col_name] <= end_date)] scatter_fig = px.scatter(filtered_df, x=x_col_name, y=y_col_name_list) return scatter_fig if __name__ == '__main__': app.run_server(debug=True) Function process_csv_variable sẽ nhận vào DataFrame, đọc config từ tên cột, xử lý data dựa theo config và sẽ trả về DataFrame sau khi xử lý. DateTime(yyyyMMdd HH:mm:ss.fff),DATA 1(minFilter=20;maxFilter=100),DATA 2(maxFilter=140),DATA 3,DATA 4,DATA 5 20211219 101010.010,10,200,178,90,110 20211219 111010.020,20,150,134,25,120 20211219 121010.030,5,130,210,11,90 20211219 131010.040,15,110,100,-97,80 20211219 141010.050,60,150,143,-17,130 20211219 151010.060,30,140,132,30,140 20211219 161010.070,20,180,167,45,150 20211219 171010.080,16,120,240,123,160 20211219 181010.090,75,190,153,40,150 20211219 191010.100,90,250,162,-10,170 20211220 001010.000,68,142,156,1,180 20211220 011010.010,40,110,134,65,130 20211220 021010.020,50,140,214,149,190 20211220 031010.030,60,150,169,-98,200 20211220 041010.040,70,160,204,-173,190 20211220 051010.050,80,170,164,-108,180 20211220 061010.060,90,180,148,150,170 20211220 071010.070,100,190,180,92,150 20211220 081010.080,110,200,268,94,160 20211220 091010.090,120,210,164,-139,140 20211220 101010.100,130,220,254,-132,130 20211221 001010.000,10,90,142,30,150 20211221 011010.010,30,100,162,55,160 20211221 021010.020,80,120,180,20,170 20211221 031010.030,70,110,176,-10,110 20211221 041010.040,50,130,194,-90,90 20211221 051010.050,60,140,202,-120,80 20211221 061010.060,90,150,164,100,70 20211221 071010.070,120,160,197,132,60 20211221 081010.080,110,170,186,40,50 20211221 091010.090,130,210,182,-130,40 20211221 101010.100,120,230,210,-100,30 Trong terminal chạy lệnh Để dễ kiểm tra kết quả chúng ta ẩn các data khác chỉ hiển thị DATA 1 Chúng ta thấy các data có value nhỏ hơn 20 và lớn hơn 100 đã bị lọc bỏ. Source codehttps://gitlab.com/bwv-hp/python-dash-sample Tài liệu tham khảohttps://dash.plotly.com/ https://pandas.pydata.org/docs/ |