Last active
July 9, 2021 14:59
-
-
Save mebaysan/39c2a7a7a17227d8bcab1055b7eda603 to your computer and use it in GitHub Desktop.
Plotly/Dash için Bulma CSS temel alınan component builder sınıfı
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import plotly.express as px | |
import plotly.graph_objects as go | |
import dash_html_components as html | |
import dash_core_components as dcc | |
import pandas as pd | |
from plotly.subplots import make_subplots | |
import math | |
class ComponentBuilder(object): | |
""" | |
Kullanmak için Dash uygulamasına Bulma CSS'i eklemelisiniz: | |
dash.Dash(__name__, | |
external_stylesheets=[ | |
"/static/node_modules/bulma/css/bulma.min.css" | |
]) | |
""" | |
def __init__(self): | |
self.PLOT_THEME = 'plotly_white' | |
self.MAIN_COLOR_1 = '#66bb6a' | |
self.MAIN_COLOR_2 = '#bcaaa4' | |
self.MAIN_TEXT_FONT_SIZE = 14 | |
self.HELPER_TEXT_FONT_SIZE = 12 | |
self.FIGURE_STYLE = { | |
'figureTextSize': self.HELPER_TEXT_FONT_SIZE, | |
'figureTitleTextSize': self.MAIN_TEXT_FONT_SIZE, | |
'paper_bgcolor': '#fff', # plotly grafik kağıdı arka plan rengi | |
'plot_bgcolor': '#fff', # plotly grafik arka plan rengi | |
'header_fill_color': self.MAIN_COLOR_1, | |
'header_font': '#FFFFFF', | |
'cells_fill_color': self.MAIN_COLOR_2, | |
'cells_font': '#F5F5F5', | |
'color_discrete_sequence': px.colors.qualitative.Dark2 | |
}, | |
self.SPECIAL_INPUT_KEY = 'Hepsi' # Verileri filtrelerken kendi ekranlarımız için bir özel key. Mesela 'Hepsi' olduğunda dropdownlara 'Hepsi' gelecek bu sayede bir değişkende bir değeri filtrelerken diğerlerinde hepsini seçebiliyoruz | |
self.EXTERNAL_STYLESHEETS = [ | |
"/static/node_modules/bulma/css/bulma.min.css", | |
"/static/css/customdash.css" | |
] # Dash instance oluştururken component builder'in style sheets'ini göndereceğiz | |
self.META_TAGS = [ | |
{"name": "viewport", | |
"content": "width=device-width, initial-scale=1"} | |
] # Dash instance oluştururken component builder'in meta tags'lerini göndereceğiz | |
self.LAYOUT_CLASSNAME = 'container' | |
self.MONTHS = ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', | |
'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'] | |
self.MONTHS_NUMBERS = [_ for _ in range(1, 13)] | |
self.DAYS = ['Pazartesi', 'Salı', 'Çarşamba', | |
'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'] | |
self.DAYS_NUMBERS = [_ for _ in range(1, 32)] | |
self.TABLE_ALIGN = 'center' # table yazı pozisyonu | |
self.TABLE_HEADER_COLOR = '#FFFFFF' # table header yazı color | |
# table header fill (arka plan) color | |
self.TABLE_HEADER_FILL_COLOR = 'grey' | |
self.TABLE_CELLS_COLOR = 'grey' # table hücre yazı color | |
# table cells fill (arka plan) color | |
self.TABLE_CELLS_FILL_COLOR = '#FFFAFA' | |
# table hücre aralarındaki çizgi renkleri | |
self.TABLE_LINE_COLOR = 'darkslategray' | |
# basic table içerisindeki metinlerin font size'ı | |
self.TABLE_STYLE = {'fontSize': self.MAIN_TEXT_FONT_SIZE} | |
self.INPUT_LABEL_STYLE = { | |
'fontSize': self.MAIN_TEXT_FONT_SIZE} # input label font | |
# input'lar içerisindeki font | |
self.INPUT_STYLE = {'fontSize': self.HELPER_TEXT_FONT_SIZE} | |
self.HELP_TEXT_STYLE = { | |
'fontSize': self.HELPER_TEXT_FONT_SIZE} # help text font | |
# button text font | |
self.BUTTON_STYLE = {'fontSize': self.MAIN_TEXT_FONT_SIZE} | |
def get_basic_table(self, headers, rows, is_in_column=True, column_class='', table_style={}): | |
""" | |
headers -> liste olarak gelecek | |
rows -> dict olarak gelecek | |
""" | |
_table_style = self.TABLE_STYLE | |
if table_style: | |
for i in table_style: | |
_table_style[i] = table_style[i] | |
table_rows = [] | |
for row in rows: | |
table_rows.append( | |
html.Tr(children=[html.Td(f'{row}'), html.Td(f'{rows[row]}')])) | |
table = html.Table(style=_table_style, className='table is-fullwidth is-hoverable', | |
children=[ | |
html.Thead(children=[ | |
html.Tr(children=[ | |
html.Th(headers[0]), | |
html.Th(headers[1]), | |
]) | |
]), | |
html.Tbody(children=table_rows) | |
]) | |
if is_in_column: | |
column = html.Div( | |
className=f'column {column_class}', children=[table]) | |
return column | |
return table | |
def get_tabs(self, tabs_style='is-fullwidth', tabs_id='tabs', tab_items={}): | |
""" | |
Tab Style için: | |
https://bulma.io/documentation/components/tabs/ | |
is-fullwidth | |
is-right | |
is-centered | |
is-small | |
is-medium | |
is-large | |
********************************* | |
tabs_id -> sayesinde sayfa içeriğini değiştirebileceğiz | |
********************************* | |
tab_items -> {'Tab Title':'Tab-Link'} | |
""" | |
_tab_items = [] | |
for tab_item in tab_items: | |
_tab_items.append( | |
html.Li(html.A(tab_item, href=tab_items[tab_item]))) | |
tabs = html.Div(className=f'tabs {tabs_style}', id=tabs_id, children=[ | |
html.Ul(children=_tab_items) | |
]) | |
return tabs | |
def get_dropdown_input(self, data_frame, filter_column, label, input_id, label_class='', label_style={}, special_key_in_option=True, default_value='self', dropdown_style={}, is_in_column=True, column_class='', help_text='', help_text_class='is-success', is_year=False, is_month=False): | |
""" | |
data_frame -> hangi veri seti | |
filter_column -> dropdown'da hangi değişkene ait veriler gözükecek | |
label -> input başlığı | |
label_class -> label'a extra class gelecek mi | |
label_style -> label'a özel stil verilecek mi | |
input_id -> callback'leri çalıştırırken hangi id ile bu elementi yakalayacağız | |
special_key_in_option -> self.SPECIAL_INPUT_KEY filtrede olacak mı (mesela Hepsi seçeneği) | |
default_value -> dropdown'un başlangıç değeri (first veya self alabilir, first alırsa ilgili değişkenin tekil değerlerinin ilk elemanını başlangıç değeri olarak set eder), max alırsa ilgili kolonun en büyük değerini alır (Yıl vb için kullanıyoruz) | |
dropdown_style -> dropdown'a css verilecekse dict olarak gelmeli | |
is_in_column -> input column içine alınacak mı | |
column_class -> column içine alınacaksa column style'ı ne olacak | |
help_text -> input altında uyarı metni yazacak mı | |
help_text_class -> help textin class'ı (Bulma'dan gelen is-primary vb) | |
is_year -> Bu dropdown yılları mı listeleyecek, öyle ise büyükten küçüğe sıralayacak | |
is_month -> Bu dropdown ayları mı listeleyecek, öyle ise bu sınıfın kendi MONTHS prop'una göre sıralanacak | |
""" | |
if type(data_frame) == list: | |
# liste içinde dict'ler -> [{'label':'Türk Lirası','value':'TL Cinsinden Proje Toplam'}] | |
options = data_frame | |
if default_value == 'first': | |
value = options[0]['value'] | |
elif default_value == 'last': | |
value = options[len(options) - 1]['value'] | |
else: | |
value = self.SPECIAL_INPUT_KEY | |
elif type(data_frame) == pd.DataFrame: # DataFrame gelirse | |
if is_year: | |
data_frame.sort_values( | |
filter_column, ascending=False, inplace=True) | |
if is_month: | |
data_frame[filter_column] = pd.Categorical( | |
data_frame[filter_column], categories=self.MONTHS, ordered=True) # eğer ay değişkeni ise gelen veri setinin ilgili değişkenini Categorical olarak işaretle ve bunu kendi ay sıralamana göre (self.MONTHS) sırala | |
data_frame.sort_values(f'{filter_column}', inplace=True) | |
OPTIONS = data_frame[f'{filter_column}'].unique() | |
options = [{'label': _, 'value': _} for _ in OPTIONS] | |
options.insert(0, {'label': self.SPECIAL_INPUT_KEY, | |
'value': self.SPECIAL_INPUT_KEY}) if special_key_in_option else options | |
if default_value == 'first': | |
value = data_frame[filter_column].unique()[0] | |
elif default_value == 'max': | |
value = data_frame[filter_column].max() | |
elif default_value == 'min': | |
value = data_frame[filter_column].min() | |
elif default_value == 'last': | |
value = data_frame[filter_column].unique()[-1] | |
elif type(default_value) == tuple: | |
value = default_value[0] | |
else: | |
value = self.SPECIAL_INPUT_KEY | |
else: | |
return TypeError | |
_label_style = self.INPUT_LABEL_STYLE | |
if label_style: | |
for i in label_style: | |
_label_style[i] = label_style[i] | |
_dropdown_style = self.INPUT_STYLE | |
if dropdown_style: | |
for i in dropdown_style: | |
_dropdown_style[i] = dropdown_style[i] | |
field = html.Div(className='field', children=[ | |
html.Label(className=f'label {label_class}', style=_label_style, children=[ | |
f'{label}' | |
]), | |
html.Div(className='control', children=[ | |
dcc.Dropdown( | |
id=input_id, | |
options=options, | |
value=value, | |
style=_dropdown_style | |
), | |
]), | |
html.P(f'{help_text}', className=f'help {help_text_class}') | |
]) | |
if is_in_column: | |
column = html.Div( | |
className=f'column {column_class}', children=[field]) | |
return column | |
return field | |
def get_input_text(self, label, input_id, input_type, label_class='', label_style={}, default_value='self', input_style={}, input_class='is-primary', is_in_column=True, column_class='', help_text='', help_text_class='is-success', help_text_style={}, editable=True): | |
""" | |
label -> input başlığı | |
label_class -> label'a extra class gelecek mi | |
label_style -> label'a özel stil verilecek mi | |
input_id -> callback'leri çalıştırırken hangi id ile bu elementi yakalayacağız | |
input_type -> number, text, email vb (dcc'den bakabilirsiniz) | |
input_style -> input'a extra css vermek ister misiniz | |
input_class -> input'a extra class vermek ister misiniz | |
default_value -> input'un başlangıç değeri | |
is_in_column -> input column içine alınacak mı | |
column_class -> column içine alınacaksa column style'ı ne olacak | |
help_text -> input altında uyarı metni yazacak mı | |
help_text_class -> help textin class'ı (Bulma'dan gelen is-primary vb) | |
editable -> True ise yazmaya izin vermez | |
""" | |
if default_value == 'self': | |
value = self.SPECIAL_INPUT_KEY | |
else: | |
value = default_value | |
_label_style = self.INPUT_LABEL_STYLE | |
if label_style: | |
for i in label_style: | |
_label_style[i] = label_style[i] | |
_input_style = self.INPUT_STYLE | |
if input_style: | |
for i in input_style: | |
_input_style[i] = input_style[i] | |
_help_text_style = self.HELP_TEXT_STYLE | |
if help_text_style: | |
for i in help_text_style: | |
_help_text_style[i] = help_text_style[i] | |
field = html.Div(className='field', children=[ | |
html.Label(className=f'label {label_class}', style=_label_style, children=[ | |
f'{label}' | |
]), | |
html.Div(className='control', children=[ | |
dcc.Input( | |
id=input_id, | |
className=f'input {input_class}', | |
type=input_type, | |
value=value, | |
style=_input_style, | |
readOnly=False if editable == True else True | |
), | |
]), | |
html.P(f'{help_text}', className=f'help {help_text_class}', | |
style=_help_text_style) | |
]) | |
if is_in_column: | |
column = html.Div( | |
className=f'column {column_class}', children=[field]) | |
return column | |
return field | |
def get_button(self, label, button_id, button_class='is-primary', button_style={}, default_nclick=1, is_in_column=False, column_class='', is_href=False, href=''): | |
""" | |
label -> buton üzerinde ne yazacak | |
button_id -> butona nasıl erişilecek (callbackler için hangi id'den tetiklenecek vb.) | |
button_class -> butona özel class verecek miyiz | |
button_style -> butona özel stil vermek istersek | |
default_nclick -> default kaç click ile initialize olacak | |
is_in_column -> column içinde mi | |
column_class -> column içindeyse column class'ı ne olacak (boş gelirse auto olarak alır) | |
is_href -> True ise button A tagı içerisine gömülür | |
href -> is_href = True ise A tagına koyulacak olan link (ref) | |
""" | |
_button_style = self.BUTTON_STYLE | |
if button_style: | |
for i in button_style: | |
_button_style[i] = button_style[i] | |
button = html.Div(className='buttons', children=[ | |
html.Button( | |
f'{label}', className=f'button {button_class}', id=button_id, n_clicks=default_nclick, style=_button_style) | |
]) | |
if is_href: | |
button = html.A(children=[button], href=href) | |
if is_in_column: | |
column = html.Div( | |
className=f'column {column_class}', children=[button]) | |
return column | |
return button | |
def get_graph_column(self, label, label_id, figure, figure_id, label_class='', column_class='', label_style={}, column_style={}): | |
""" | |
label -> grafiğin başlığı (ya da üzerinde ne gözükecekse / bu sayede grafiğin oluşturulduğu svg içine title atmadan daha fazla yer kazanabileceğiz) | |
label_id -> filtrelerden gelen verilere göre dinamik olarak title oluşturmak istersek çıktıyı Output() ile göndermek için bu id'e ihtiyacımız olacak | |
figure -> column içindeki dcc.Graph() altında gözükecek olan figure | |
figure_id -> figure'e output vermek için kullanacağımız id | |
label_class -> labellara class vermek istersek kullanacağımız parametre | |
column_class -> columnlara class vermek istersek kullanacağımız parametre | |
label_style -> label'a özel olarak stil vermek istersek | |
column_style -> column'a özel olarak stil vermek istersek | |
""" | |
column = html.Div(className=f'column {column_class}', style=column_style, children=[ | |
html.Label(f'{label}', id=label_id, | |
style=label_style, | |
className=f'label {label_class}'), | |
dcc.Graph(id=figure_id, figure=figure)]) | |
return column | |
def get_sankey(self, data, path, value_col): | |
# bir veri seti alır, ve gelen path parametresine göre sankey diyagramı için dict oluşturur | |
# value_col parametresine göre diyagram üzerinde göstereceği value'leri set eder | |
""" | |
Örnek bir kullanım | |
my_sankey = get_sankey( | |
sankey_df,['Kategori','Projeli_mi','Ürün Adı'],'Miktar') | |
fig = go.Figure(data=[go.Sankey( | |
node = dict( | |
pad = 15, | |
thickness = 20, | |
line = dict(color = "black", width = 0.5), | |
label = my_sankey['label'], | |
color = "blue" | |
), | |
link = dict( | |
source = my_sankey['source'], | |
target = my_sankey['target'], | |
value = my_sankey['value'] | |
))]) | |
""" | |
sankey_data = { | |
'label': [], | |
'source': [], | |
'target': [], | |
'value': [] | |
} | |
counter = 0 | |
while (counter < len(path) - 1): | |
for parent in data[path[counter]].unique(): | |
sankey_data['label'].append(parent) | |
for sub in data[data[path[counter]] == parent][path[counter+1]].unique(): | |
sankey_data['source'].append( | |
sankey_data['label'].index(parent)) | |
sankey_data['label'].append(sub) | |
sankey_data['target'].append( | |
sankey_data['label'].index(sub)) | |
sankey_data['value'].append( | |
data[data[path[counter+1]] == sub][value_col].sum()) | |
counter += 1 | |
return sankey_data | |
def get_data_table(self, title, headers, values, row_size=0, is_autosize=False, row_height=30, is_return_trace=False, is_fill_color=False, color_palette=[]): | |
"""Özet | |
Args: | |
title (string): oluşacak grafiğin başlığı | |
headers (list): tablonun başlıklarını liste olarak alır | |
values (list): tablonun hücrelerini (cells) liste olarak alır. her sütun bir liste içerisinde gelmelidir. [[Sütun 1], [Sütun 2], [Sütun 3]...] | |
row_size(int): veri seti içerisindeki satır adedi | |
is_autosize(bool): tablo otomatik olarak resize olsun mu | |
row_height(int): bir satırın yüksekliği ne kadar olsun | |
is_return_trace(bool): True olursa trace'i döner, False olursa figure döner | |
is_fill_color(bool): True olursa color_palette parametresini color olarak set eder | |
color_palette(list): satırları boyayacağımız renk kodlarını içerir, her satıra karşılık gelen bir renk kodu içermeli (liste içerisinde liste gelmeli) | |
- Ör: color_palette = [df['Renk']] | |
Returns: | |
figure: graph objects figure döner (table) | |
""" | |
trace = go.Table( | |
header=dict(values=headers, | |
fill_color=self.TABLE_HEADER_FILL_COLOR, | |
font={'color': self.TABLE_HEADER_COLOR}, | |
line_color=self.TABLE_LINE_COLOR, | |
align=self.TABLE_ALIGN), | |
cells=dict(values=values, | |
font={'color': self.TABLE_CELLS_COLOR}, | |
# line_color=self.TABLE_LINE_COLOR if is_fill_color == False else color_palette, | |
line_color=self.TABLE_LINE_COLOR, | |
fill_color=self.TABLE_CELLS_FILL_COLOR if is_fill_color == False else color_palette, | |
align=self.TABLE_ALIGN | |
)) | |
if is_return_trace: | |
return trace | |
layout = go.Layout(title=title, | |
template=self.PLOT_THEME) | |
fig = go.Figure(data=[trace], layout=layout) | |
if is_autosize: | |
height = row_size * row_height | |
if row_size <= 13: | |
height = 450 | |
elif row_size >= 26: | |
height = height + height / 3 | |
fig.update_layout(height=height) | |
return fig | |
def get_download_button(self, id, button_text='Veri Setini İndir', button_style={}, default_nclicks=0): | |
""" | |
id = button id | |
button_text = Buton üzerinde gözükecek olan text | |
button_style = Dict olarak gelir. Dash yapısına uygun olmalı | |
default_nclicks = default olarak kaç click ile gelecek | |
""" | |
style = {} | |
# style = {'float': 'left'} | |
for stl in button_style: | |
style[stl] = button_style[stl] | |
button = html.Div(style=style, className='navbar-item has-dropdown is-hoverable baysan-download-div', children=[ | |
html.A(button_text, id=id, className='navbar-link baysan-download-btn', | |
n_clicks=default_nclicks) | |
]) | |
return button | |
def get_subplot_tables_three_columns(self, df, main_col, first_col, second_col, third_col, _titles, _print_grid=False): | |
""" | |
df = gruplanmış veri seti | |
main_col = hangi kolona göre gruplanacak | |
first_col = ilk kolon | |
second_col = ikinci kolon | |
third_col = üçüncü kolon | |
_titles = liste olarak title'lar | |
Bu fonksiyon kontrol_denetim altındaki aylık işlem özet ekranı için yazıldı. | |
Daha da güzelleştirilebilir. | |
Eğer bu yorumu okuyorsanız gelişmişine ihtiyaç duymadığımızdan sadece o ekrandaki tablolara özel kalmıştır demektir :) | |
""" | |
row_count = math.ceil(len(df[main_col].unique()) / 2) | |
col_count = 2 | |
fig_specs = [] # hangi subplot nereye gelecek | |
for i in range(row_count): | |
fig_specs.append([{"type": "table"} for _ in range(col_count)]) | |
titles = [] | |
for baskanlik in df[main_col].unique(): | |
dummy = df.groupby(main_col, as_index=False).sum() | |
titles.append("{} (%{})".format(baskanlik, round( | |
dummy[dummy[main_col] == baskanlik][third_col].sum(), 2))) | |
fig = make_subplots(rows=row_count, cols=col_count, | |
specs=fig_specs, subplot_titles=titles, vertical_spacing=0.08, print_grid=_print_grid) | |
row_counter = 1 | |
col_counter = 1 | |
counter = 0 | |
for trace in df[main_col].unique(): | |
fig.add_trace( | |
self.get_data_table( | |
trace, _titles, [ | |
df[df[main_col] == trace][first_col], | |
df[df[main_col] == trace][second_col], | |
df[df[main_col] == trace][third_col].apply( | |
lambda x: round(x, 2)) | |
], is_return_trace=True), | |
row=row_counter, | |
col=col_counter | |
) | |
counter += 1 | |
if counter % 2 == 1: | |
col_counter = 2 | |
else: | |
col_counter = 1 | |
if counter % col_count == 0: | |
row_counter += 1 | |
return fig | |
def update_figure_layout(self, fig, | |
paper_bgcolor=None, | |
plot_bgcolor=None, | |
plot_theme=None, | |
font_size=None, | |
title_font_size=None, | |
autosize=True, | |
margin={}, | |
margin_autoexpand=True, | |
title_x=0.5, | |
title_y=0.9, | |
width=None, | |
height=None | |
): | |
""" | |
parametre olarak aldığı figürü update eder ve döndürür | |
""" | |
fig.update_layout( | |
paper_bgcolor=self.FIGURE_STYLE[0]['paper_bgcolor'] if paper_bgcolor == None else paper_bgcolor, | |
plot_bgcolor=self.FIGURE_STYLE[0]['plot_bgcolor'] if plot_bgcolor == None else plot_bgcolor, | |
template=self.PLOT_THEME if plot_theme == None else plot_theme, | |
font_size=self.FIGURE_STYLE[0]['figureTextSize'] if font_size == None else font_size, | |
title_font_size=self.FIGURE_STYLE[0]['figureTitleTextSize'] if title_font_size == None else title_font_size, | |
autosize=autosize, | |
margin=margin, | |
margin_autoexpand=margin_autoexpand, | |
title_x=title_x, | |
title_y=title_y | |
) | |
if width != None: | |
fig.update_layout(width=width) | |
if height != None: | |
fig.update_layout(height=height) | |
return fig | |
def create_figure_layout(self, title): | |
""" | |
gelen başlığa göre bir layout oluşturur | |
""" | |
layout = go.Layout(title=title, template=self.PLOT_THEME) | |
return layout | |
def create_columns_with_figures(self, ids=[], styles=None): | |
""" | |
her id için column içinde bir Graph oluşturur ve columns div ile sarmalar | |
styles eğer gelmezse klasik column style ekler, eğer column'lara class verilmek istenirse ilgili id'nin indexini ilgili class ile göndermek gerek, | |
diğerlerini '' boş str olarak gönder | |
""" | |
graphs = [] | |
if styles == None: | |
styles = ['column ' for _ in ids] | |
else: | |
styles = [f'column {_}' for _ in styles] | |
ids_styles = dict(zip(ids, styles)) | |
for id_style in ids_styles: | |
graphs.append( | |
html.Div(className=ids_styles[id_style], children=[dcc.Graph(id=id_style)])) | |
columns = html.Div(className='columns', children=graphs) | |
return columns |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment