-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
210 lines (183 loc) · 6.91 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
from components.app_config import create_app, external_stylesheets
from components.html_components import title_card, modal
from dash import dcc, html, no_update, callback_context
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from loguru import logger
from utils.dataframes import ep1_df, ep2_df, ep3_df
from utils.functions import generate_column_defs
import dash_ag_grid as dag
import dash_bootstrap_components as dbc
import pandas as pd
# Create the Dash app
app = create_app(
external_stylesheets = external_stylesheets,
external_scripts = [
{ # Plausible analytics
'src': "https://plausible.automateordie.io/js/plausible.js",
'data-domain': "enemies.xenosaga.games",
'defer': True,
'type': 'application/javascript'
},
]
)
# Set the page title
app.title = "Xenosaga Enemy Database"
app.description = "A searchable and sortable table of all enemies in the Xenosaga series, organized by game."
# For Gunicorn
server = app.server
app.layout = html.Div(
[
dcc.Location(id='url', refresh=False),
dcc.Store(id='clicked-cell-unique-value'),
dcc.Store(id='active-tab-data'),
html.Div(title_card),
# Use dcc.Tabs for episode selection instead of buttons
dbc.Tabs(
id='tabs',
active_tab='ep1', # Set default active tab to Episode I
children=[
dbc.Tab(label='Episode I', tab_id='ep1'),
dbc.Tab(label='Episode II', tab_id='ep2'),
dbc.Tab(label='Episode III', tab_id='ep3'),
],
style={'flex': '0 0 auto'}, # Style adjustments for tabs
),
# Container for the grid; make sure it's visible and properly styled
html.Div(
dag.AgGrid(
id='grid',
className="ag-theme-alpine-dark",
style={
'width': '100%',
'height': 'calc(100vh - 200px)',
},
),
id='grid-container',
style={'flex': '1 1 auto', 'overflow': 'auto'}, # Allow horizontal and vertical scrolling
),
modal,
],
style={
'display': 'flex',
'flexDirection': 'column',
'height': '100vh'
},
)
# A callback to generate the grid (lazy load) and the column definitions based on the selected tab
@app.callback(
[Output('grid', 'rowData'), Output('grid', 'columnDefs'), Output('active-tab-data', 'data')],
[Input('tabs', 'active_tab')]
)
def update_grid_data_and_columns(active_tab):
if active_tab == 'ep1':
data = ep1_df
elif active_tab == 'ep2':
data = ep2_df
elif active_tab == 'ep3':
data = ep3_df
else: # Handle the case where the active tab is not one of the above
return [], [] # Return empty data and column definitions
rowData = data.to_dict('records')
columnDefs = generate_column_defs(data)
return rowData, columnDefs, rowData
# Create a callback to update the column size to autoSize
# Gets triggered when the columnDefs property of the grid changes. This callback will then set the columnSize property to "autoSize"
@app.callback(
Output('grid', 'columnSize'),
[Input('grid', 'columnDefs')]
)
def update_column_size(_):
return "responsiveSizeToFit"
# Create a callback to open a modal when a row is selected in the grid
# Based on https://dashaggrid.pythonanywhere.com/other-examples/popup-from-cell-click
@app.callback(
Output("modal", "is_open"),
Output("modal-header", "children"),
Output("modal-content", "children"),
[
Input("grid", "cellClicked"),
Input("close", "n_clicks"),
],
[
State("modal", "is_open"),
State("grid", "rowData")
]
)
def open_and_populate_modal(cell_clicked_data, close_btn_clicks, modal_open, grid_data):
ctx = callback_context
if not ctx.triggered:
return no_update, no_update, no_update
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
if trigger_id == 'close':
return False, no_update, no_update
if trigger_id == 'grid':
if not cell_clicked_data or 'rowIndex' not in cell_clicked_data:
raise PreventUpdate
# Assuming 'rowId' corresponds to the index in 'grid_data' and contains the 'uuid'
row_id = cell_clicked_data['rowId']
clicked_uuid = grid_data[int(row_id)]['uuid']
# Populate the modal with the correct data
data = {"uuid": clicked_uuid}
if not data:
raise PreventUpdate
# Consolidate dataset lookup into a single statement with a clearer structure
datasets = {'ep1': ep1_df, 'ep2': ep2_df, 'ep3': ep3_df}
found_df = next((df for df_name, df in datasets.items() if data["uuid"] in df['uuid'].values), None)
if found_df is None:
logger.error(f"UUID {data['uuid']} not found in any dataset.")
return True, html.P("Error: Details not found for the selected enemy.", className="modal-error-message")
# Efficiently retrieve the selected row
selected_row = found_df.loc[found_df['uuid'] == data['uuid']].iloc[0]
def format_value(value):
"""Format the value for display. If the value is a number, format it with commas. Otherwise, return the value as is."""
if value is None or value == '':
return 'N/A'
try:
numeric_value = float(value)
if numeric_value.is_integer():
return f"{int(numeric_value):,}"
return f"{numeric_value:,}"
except (ValueError, TypeError):
return value
def apply_element_style(text):
"""Colorize the text based on the element. Preserves spaces and commas."""
color_styles = {
"Lightning": "yellow",
"Fire": "red",
"Ice": "lightblue",
"Yes": "green",
"No": "red",
"Cannot": "red",
}
parts = text.split(", ")
spans = []
for i, part in enumerate(parts):
color = color_styles.get(part, None)
if color:
spans.append(html.Span(part, style={'color': color}))
else:
spans.append(html.Span(part))
if i < len(parts) - 1:
spans.append(", ")
return spans
# Retrieve the enemy's name
enemy_name = selected_row['Name']
# Remove the 'Name' key-value pair from the selected_row dictionary
# This is to prevent the 'Name' key from being displayed in the modal body, as it's already displayed in the modal header
selected_row = selected_row.drop('Name')
# Streamline content generation by using Dash HTML components for better layout control
content = []
for key, value in selected_row.items():
if key != "uuid":
if isinstance(value, str):
spans = apply_element_style(value)
content.append(html.Div([html.B(f"{key}: "), *spans], style={'margin-bottom': '10px'}))
else:
content.append(html.Div([html.B(f"{key}: "), html.Span(f"{format_value(value) if pd.notnull(value) else 'N/A'}")], style={'margin-bottom': '10px'}))
logger.debug(f"Selected enemy stats: {content}")
return True, html.H4(enemy_name), html.Div(content, className="modal-content-wrapper")
return no_update, no_update, no_update
# Run the app if running locally
if __name__ == '__main__':
app.run_server(debug=True)