Spaces:
Running
Running
Add app configuration
Browse files- .gitignore +144 -0
- Dockerfile +18 -0
- README.md +24 -5
- app.py +231 -0
- assets/css/custom.css +64 -0
- assets/favicon.ico +0 -0
- assets/js/dashAgGridComponentFunctions.js +68 -0
- assets/logo.svg +3 -0
- requirements.in +4 -0
- requirements.txt +130 -0
- utils/__init__.py +1 -0
- utils/_helper.py +69 -0
.gitignore
ADDED
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# npm node_modules
|
2 |
+
node_modules/
|
3 |
+
|
4 |
+
# Ipynb
|
5 |
+
ipynb_checkpoints
|
6 |
+
*/.ipynb_checkpoints/*
|
7 |
+
|
8 |
+
# IPython
|
9 |
+
profile_default/
|
10 |
+
ipython_config.py
|
11 |
+
|
12 |
+
# Byte-compiled / optimized / DLL files
|
13 |
+
__pycache__/
|
14 |
+
*.py[cod]
|
15 |
+
*$py.class
|
16 |
+
|
17 |
+
# C extensions
|
18 |
+
*.so
|
19 |
+
|
20 |
+
# Distribution / packaging
|
21 |
+
.Python
|
22 |
+
build/
|
23 |
+
develop-eggs/
|
24 |
+
dist/
|
25 |
+
downloads/
|
26 |
+
eggs/
|
27 |
+
.eggs/
|
28 |
+
lib/
|
29 |
+
lib64/
|
30 |
+
parts/
|
31 |
+
sdist/
|
32 |
+
var/
|
33 |
+
wheels/
|
34 |
+
share/python-wheels/
|
35 |
+
*.egg-info/
|
36 |
+
.installed.cfg
|
37 |
+
*.egg
|
38 |
+
MANIFEST
|
39 |
+
|
40 |
+
# macOS
|
41 |
+
*.DS_Store
|
42 |
+
.DS_Store
|
43 |
+
.AppleDouble
|
44 |
+
.LSOverride
|
45 |
+
.Trashes
|
46 |
+
|
47 |
+
# PyInstaller
|
48 |
+
*.manifest
|
49 |
+
*.spec
|
50 |
+
|
51 |
+
# Installer logs
|
52 |
+
pip-log.txt
|
53 |
+
pip-delete-this-directory.txt
|
54 |
+
|
55 |
+
# Unit test / coverage reports
|
56 |
+
htmlcov/
|
57 |
+
.tox/
|
58 |
+
.nox/
|
59 |
+
.coverage
|
60 |
+
.coverage.*
|
61 |
+
.cache
|
62 |
+
nosetests.xml
|
63 |
+
coverage.xml
|
64 |
+
*.cover
|
65 |
+
*.py,cover
|
66 |
+
.hypothesis/
|
67 |
+
.pytest_cache/
|
68 |
+
cover/
|
69 |
+
|
70 |
+
# Translations
|
71 |
+
*.mo
|
72 |
+
*.pot
|
73 |
+
|
74 |
+
# Django
|
75 |
+
*.log
|
76 |
+
local_settings.py
|
77 |
+
db.sqlite3
|
78 |
+
db.sqlite3-journal
|
79 |
+
|
80 |
+
# Flask
|
81 |
+
instance/
|
82 |
+
.webassets-cache
|
83 |
+
|
84 |
+
# Scrapy
|
85 |
+
.scrapy
|
86 |
+
|
87 |
+
# PyBuilder
|
88 |
+
.pybuilder/
|
89 |
+
target/
|
90 |
+
|
91 |
+
# IntelliJ
|
92 |
+
.idea/
|
93 |
+
*.iml
|
94 |
+
out/
|
95 |
+
.idea_modules/
|
96 |
+
|
97 |
+
# Vscode
|
98 |
+
.vscode/
|
99 |
+
|
100 |
+
# PEP 582
|
101 |
+
__pypackages__/
|
102 |
+
|
103 |
+
# Celery
|
104 |
+
celerybeat-schedule
|
105 |
+
celerybeat.pid
|
106 |
+
|
107 |
+
# SageMath
|
108 |
+
*.sage.py
|
109 |
+
|
110 |
+
# Environments
|
111 |
+
.env
|
112 |
+
.venv
|
113 |
+
env/
|
114 |
+
venv/
|
115 |
+
ENV/
|
116 |
+
env.bak/
|
117 |
+
venv.bak/
|
118 |
+
|
119 |
+
# Spyder
|
120 |
+
.spyderproject
|
121 |
+
.spyproject
|
122 |
+
|
123 |
+
# Rope
|
124 |
+
.ropeproject
|
125 |
+
|
126 |
+
# Mkdocs
|
127 |
+
/site
|
128 |
+
|
129 |
+
# Mypy
|
130 |
+
.mypy_cache/
|
131 |
+
.dmypy.json
|
132 |
+
dmypy.json
|
133 |
+
|
134 |
+
# Pyre
|
135 |
+
.pyre/
|
136 |
+
|
137 |
+
# Pytype
|
138 |
+
.pytype/
|
139 |
+
|
140 |
+
# Cython
|
141 |
+
cython_debug/
|
142 |
+
|
143 |
+
# Ruff
|
144 |
+
.ruff_cache/
|
Dockerfile
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
|
2 |
+
# you will also find guides on how best to write your Dockerfile
|
3 |
+
|
4 |
+
FROM python:3.12
|
5 |
+
|
6 |
+
RUN useradd -m -u 1000 user
|
7 |
+
|
8 |
+
WORKDIR /app
|
9 |
+
|
10 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
11 |
+
|
12 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
13 |
+
|
14 |
+
COPY --chown=user . /app
|
15 |
+
|
16 |
+
EXPOSE 7860
|
17 |
+
|
18 |
+
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "app:server"]
|
README.md
CHANGED
@@ -1,12 +1,31 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
colorTo: blue
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: apache-2.0
|
9 |
-
short_description:
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Games Dashboard
|
3 |
+
emoji: 📊
|
4 |
+
colorFrom: blue
|
5 |
colorTo: blue
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: apache-2.0
|
9 |
+
short_description: Figure Friday Week 5 - Games
|
10 |
---
|
11 |
|
12 |
+
# Games Dashboard
|
13 |
+
|
14 |
+
This dashboard is a part of the Figure Friday series.
|
15 |
+
This week's Figure Friday: https://community.plotly.com/t/figure-friday-2025-week-5/90285
|
16 |
+
|
17 |
+
**Created by:** [Huong Li Nguyen](https://github.com/huong-li-nguyen)
|
18 |
+
|
19 |
+
---
|
20 |
+
|
21 |
+
### 🚀 Vizro features applied
|
22 |
+
|
23 |
+
- [Vizro tutorial on pages, layouts and dashboards](https://vizro.readthedocs.io/en/stable/pages/tutorials/explore-components/)
|
24 |
+
- [Custom charts](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-charts/)
|
25 |
+
- [Custom CSS](https://vizro.readthedocs.io/en/stable/pages/user-guides/assets/)
|
26 |
+
|
27 |
+
### 🖥️ App demo
|
28 |
+
|
29 |
+
<img src="./images/nguyen-figure-friday-5.gif" alt="Gif to KPI dashboard" width="600">
|
30 |
+
|
31 |
+
---
|
app.py
ADDED
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Example to show dashboard configuration."""
|
2 |
+
|
3 |
+
"""Dev app to try things out."""
|
4 |
+
|
5 |
+
import pandas as pd
|
6 |
+
import vizro.models as vm
|
7 |
+
from vizro import Vizro
|
8 |
+
import vizro.plotly.express as px
|
9 |
+
from dash import get_asset_url, html
|
10 |
+
import dash_bootstrap_components as dbc
|
11 |
+
|
12 |
+
from vizro.models.types import capture
|
13 |
+
from vizro.tables import dash_ag_grid
|
14 |
+
from utils.helper import categorize_price, convert_price_to_numeric, columnDefs
|
15 |
+
from vizro.figures import kpi_card
|
16 |
+
|
17 |
+
|
18 |
+
# Custom charts are required because of the post-update calls
|
19 |
+
@capture("graph")
|
20 |
+
def bar(data_frame, top_n=20, **kwargs):
|
21 |
+
data_top = data_frame.head(top_n)
|
22 |
+
fig = px.bar(data_top, **kwargs)
|
23 |
+
fig.update_layout(title=f"Top {top_n} Games by {kwargs['x']}", yaxis_title="", yaxis_autorange="reversed")
|
24 |
+
return fig
|
25 |
+
|
26 |
+
|
27 |
+
@capture("graph")
|
28 |
+
def scatter(data_frame, top_n=20, **kwargs):
|
29 |
+
data_top = data_frame.head(top_n)
|
30 |
+
fig = px.scatter(data_top, **kwargs)
|
31 |
+
fig.update_layout(
|
32 |
+
title=f"Top {top_n} games by {kwargs['y']}. Each bubble represents a single game. The bigger the bubble, the more players."
|
33 |
+
)
|
34 |
+
|
35 |
+
return fig
|
36 |
+
|
37 |
+
|
38 |
+
@capture("graph")
|
39 |
+
def treemap(data_frame, top_n=20, **kwargs):
|
40 |
+
data_top = data_frame.head(top_n)
|
41 |
+
fig = px.treemap(data_top, **kwargs)
|
42 |
+
fig.update_traces(root_color="lightgrey")
|
43 |
+
fig.update_layout(title=f"Top {top_n} games by {kwargs['values']}. The bigger the square, the more players.")
|
44 |
+
return fig
|
45 |
+
|
46 |
+
|
47 |
+
# Tidy data set
|
48 |
+
players = pd.read_csv(
|
49 |
+
"https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-5/Steam%20Top%20100%20Played%20Games%20-%20List.csv"
|
50 |
+
)
|
51 |
+
players["Price Category"] = players["Price"].apply(categorize_price)
|
52 |
+
players["Price Numeric"] = players["Price"].apply(convert_price_to_numeric)
|
53 |
+
players["Current Players"] = pd.to_numeric(players["Current Players"].str.replace(",", ""))
|
54 |
+
players["Peak Today"] = pd.to_numeric(players["Peak Today"].str.replace(",", ""))
|
55 |
+
|
56 |
+
|
57 |
+
tabs = vm.Tabs(
|
58 |
+
tabs=[
|
59 |
+
vm.Container(
|
60 |
+
title="Bar",
|
61 |
+
components=[
|
62 |
+
vm.Graph(
|
63 |
+
id="bar",
|
64 |
+
figure=bar(
|
65 |
+
players,
|
66 |
+
y="Name",
|
67 |
+
x="Current Players",
|
68 |
+
title="Current Players by Game",
|
69 |
+
orientation="h",
|
70 |
+
color="Price Numeric",
|
71 |
+
),
|
72 |
+
)
|
73 |
+
],
|
74 |
+
),
|
75 |
+
vm.Container(
|
76 |
+
title="Treemap",
|
77 |
+
components=[
|
78 |
+
vm.Graph(
|
79 |
+
id="treemap",
|
80 |
+
figure=treemap(
|
81 |
+
players, path=[px.Constant("All"), "Name"], values="Current Players", color="Price Numeric"
|
82 |
+
),
|
83 |
+
)
|
84 |
+
],
|
85 |
+
),
|
86 |
+
vm.Container(
|
87 |
+
title="Bubble",
|
88 |
+
components=[
|
89 |
+
vm.Graph(
|
90 |
+
id="bubble",
|
91 |
+
figure=scatter(
|
92 |
+
players,
|
93 |
+
x="Price Numeric",
|
94 |
+
y="Current Players",
|
95 |
+
size="Current Players",
|
96 |
+
size_max=50,
|
97 |
+
opacity=0.5,
|
98 |
+
color="Price Numeric",
|
99 |
+
),
|
100 |
+
)
|
101 |
+
],
|
102 |
+
),
|
103 |
+
],
|
104 |
+
)
|
105 |
+
|
106 |
+
|
107 |
+
players_page = vm.Page(
|
108 |
+
title="Total Players 🎮",
|
109 |
+
layout=vm.Layout(grid=[[0, 1, 2, 3, 4]] + [[5, 5, 5, 5, 5]] * 5),
|
110 |
+
components=[
|
111 |
+
vm.Figure(
|
112 |
+
id="kpi-1",
|
113 |
+
figure=kpi_card(
|
114 |
+
data_frame=players[players["Price Category"] == "Free To Play"],
|
115 |
+
value_column="Current Players",
|
116 |
+
value_format="{value:,}",
|
117 |
+
icon="groups",
|
118 |
+
title="Free to play",
|
119 |
+
),
|
120 |
+
),
|
121 |
+
vm.Figure(
|
122 |
+
id="kpi-2",
|
123 |
+
figure=kpi_card(
|
124 |
+
data_frame=players[players["Price Category"] == "Less than £15"],
|
125 |
+
value_column="Current Players",
|
126 |
+
value_format="{value:,}",
|
127 |
+
icon="group",
|
128 |
+
title="Less than £15",
|
129 |
+
),
|
130 |
+
),
|
131 |
+
vm.Figure(
|
132 |
+
id="kpi-3",
|
133 |
+
figure=kpi_card(
|
134 |
+
data_frame=players[players["Price Category"] == "£15-£30"],
|
135 |
+
value_column="Current Players",
|
136 |
+
value_format="{value:,}",
|
137 |
+
icon="person",
|
138 |
+
title="£15-£30",
|
139 |
+
),
|
140 |
+
),
|
141 |
+
vm.Figure(
|
142 |
+
id="kpi-4",
|
143 |
+
figure=kpi_card(
|
144 |
+
data_frame=players[players["Price Category"] == "£30-£45"],
|
145 |
+
value_column="Current Players",
|
146 |
+
value_format="{value:,}",
|
147 |
+
icon="people",
|
148 |
+
title="£30-£45",
|
149 |
+
),
|
150 |
+
),
|
151 |
+
vm.Figure(
|
152 |
+
id="kpi-5",
|
153 |
+
figure=kpi_card(
|
154 |
+
data_frame=players[players["Price Category"] == "More than 45£"],
|
155 |
+
value_column="Current Players",
|
156 |
+
value_format="{value:,}",
|
157 |
+
icon="groups",
|
158 |
+
title="More than 45£",
|
159 |
+
),
|
160 |
+
),
|
161 |
+
tabs,
|
162 |
+
],
|
163 |
+
controls=[
|
164 |
+
vm.Parameter(
|
165 |
+
targets=[
|
166 |
+
"bar.x",
|
167 |
+
"treemap.values",
|
168 |
+
"bubble.y",
|
169 |
+
"kpi-1.value_column",
|
170 |
+
"kpi-2.value_column",
|
171 |
+
"kpi-3.value_column",
|
172 |
+
"kpi-4.value_column",
|
173 |
+
"kpi-5.value_column",
|
174 |
+
],
|
175 |
+
selector=vm.RadioItems(
|
176 |
+
options=["Current Players", "Peak Today"],
|
177 |
+
title="Select metric in all charts",
|
178 |
+
),
|
179 |
+
),
|
180 |
+
vm.Parameter(
|
181 |
+
targets=["bar.top_n", "bubble.top_n", "treemap.top_n"],
|
182 |
+
selector=vm.Slider(min=10, max=50, step=5, value=20, title="Select top N:"),
|
183 |
+
),
|
184 |
+
vm.Filter(
|
185 |
+
column="Price Category",
|
186 |
+
selector=vm.Checklist(
|
187 |
+
options=["Free To Play", "Less than £15", "£15-£30", "£30-£45", "More than 45£"],
|
188 |
+
title="Select price category:",
|
189 |
+
),
|
190 |
+
),
|
191 |
+
vm.Filter(column="Name", selector=vm.Dropdown(title="Select game:")),
|
192 |
+
],
|
193 |
+
)
|
194 |
+
|
195 |
+
games_page = vm.Page(
|
196 |
+
title="Most played games 👾",
|
197 |
+
components=[
|
198 |
+
vm.AgGrid(
|
199 |
+
figure=dash_ag_grid(players, columnDefs=columnDefs, dashGridOptions={"rowHeight": 56, "pagination": True})
|
200 |
+
)
|
201 |
+
],
|
202 |
+
)
|
203 |
+
|
204 |
+
dashboard = vm.Dashboard(
|
205 |
+
title="Figure Friday - Week 5",
|
206 |
+
pages=[players_page, games_page],
|
207 |
+
navigation=vm.Navigation(
|
208 |
+
nav_selector=vm.NavBar(
|
209 |
+
items=[
|
210 |
+
vm.NavLink(label="Players", pages=["Total Players 🎮"], icon="Person Play"),
|
211 |
+
vm.NavLink(label="Games", pages=["Most played games 👾"], icon="Stadia Controller"),
|
212 |
+
]
|
213 |
+
)
|
214 |
+
),
|
215 |
+
)
|
216 |
+
|
217 |
+
app = Vizro().build(dashboard)
|
218 |
+
app.dash.layout.children.append(
|
219 |
+
dbc.NavLink(
|
220 |
+
["Made with ", html.Img(src=get_asset_url("logo.svg"), id="banner", alt="Vizro logo"), "vizro"],
|
221 |
+
href="https://github.com/mckinsey/vizro",
|
222 |
+
target="_blank",
|
223 |
+
className="anchor-container",
|
224 |
+
)
|
225 |
+
)
|
226 |
+
|
227 |
+
|
228 |
+
server = app.dash.server
|
229 |
+
|
230 |
+
if __name__ == "__main__":
|
231 |
+
app.run()
|
assets/css/custom.css
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#page-header {
|
2 |
+
padding-left: 8px;
|
3 |
+
}
|
4 |
+
|
5 |
+
.form-label {
|
6 |
+
font-weight: 600;
|
7 |
+
}
|
8 |
+
|
9 |
+
|
10 |
+
#kpi-1 .card-kpi {
|
11 |
+
border-left: 4px solid #afe7f9;
|
12 |
+
}
|
13 |
+
|
14 |
+
#kpi-2 .card-kpi {
|
15 |
+
border-left: 4px solid #3bbef1;
|
16 |
+
}
|
17 |
+
|
18 |
+
|
19 |
+
#kpi-3 .card-kpi {
|
20 |
+
border-left: 4px solid #0d8ed1;
|
21 |
+
}
|
22 |
+
|
23 |
+
#kpi-4 .card-kpi {
|
24 |
+
border-left: 4px solid #0061a4;
|
25 |
+
}
|
26 |
+
|
27 |
+
#kpi-5 .card-kpi {
|
28 |
+
border-left: 4px solid #003875;
|
29 |
+
}
|
30 |
+
|
31 |
+
.anchor-container {
|
32 |
+
align-items: center;
|
33 |
+
background: var(--text-primary);
|
34 |
+
border-top-left-radius: 8px;
|
35 |
+
bottom: 0;
|
36 |
+
color: var(--text-primary-inverted) !important;
|
37 |
+
display: flex;
|
38 |
+
font-size: 0.8rem;
|
39 |
+
font-weight: 500;
|
40 |
+
height: 24px;
|
41 |
+
padding: 0 12px;
|
42 |
+
position: fixed;
|
43 |
+
right: 0;
|
44 |
+
}
|
45 |
+
|
46 |
+
.anchor-container:focus,
|
47 |
+
.anchor-container:hover {
|
48 |
+
background: var(--text-secondary);
|
49 |
+
color: var(--text-primary-inverted);
|
50 |
+
}
|
51 |
+
|
52 |
+
img#banner {
|
53 |
+
height: 16px;
|
54 |
+
}
|
55 |
+
|
56 |
+
.ag-cell {
|
57 |
+
align-items: center;
|
58 |
+
display: flex;
|
59 |
+
justify-content: center;
|
60 |
+
}
|
61 |
+
|
62 |
+
.badge {
|
63 |
+
font-size: 12px;
|
64 |
+
}
|
assets/favicon.ico
ADDED
|
assets/js/dashAgGridComponentFunctions.js
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
var dagcomponentfuncs = (window.dashAgGridComponentFunctions =
|
2 |
+
window.dashAgGridComponentFunctions || {});
|
3 |
+
|
4 |
+
dagcomponentfuncs.ImgThumbnail = function (props) {
|
5 |
+
const { setData, data } = props;
|
6 |
+
|
7 |
+
function onClick() {
|
8 |
+
setData(props.value);
|
9 |
+
}
|
10 |
+
|
11 |
+
return React.createElement(
|
12 |
+
"div",
|
13 |
+
{
|
14 |
+
style: {
|
15 |
+
width: "100%",
|
16 |
+
height: "100%",
|
17 |
+
display: "flex",
|
18 |
+
alignItems: "center",
|
19 |
+
},
|
20 |
+
},
|
21 |
+
React.createElement("img", {
|
22 |
+
onClick: onClick,
|
23 |
+
style: { width: "100%", height: "auto" },
|
24 |
+
src: props.value,
|
25 |
+
}),
|
26 |
+
);
|
27 |
+
};
|
28 |
+
|
29 |
+
// use for making dbc.Button without icons
|
30 |
+
dagcomponentfuncs.DBC_Button = function (props) {
|
31 |
+
const { setData } = props;
|
32 |
+
|
33 |
+
function onClick() {
|
34 |
+
setData();
|
35 |
+
}
|
36 |
+
|
37 |
+
return React.createElement(
|
38 |
+
window.dash_bootstrap_components.Button,
|
39 |
+
{
|
40 |
+
onClick,
|
41 |
+
href: props.value,
|
42 |
+
external_link: true,
|
43 |
+
target: "_blank",
|
44 |
+
},
|
45 |
+
"Visit Store →",
|
46 |
+
);
|
47 |
+
};
|
48 |
+
|
49 |
+
// BADGE INSIDE AG GRID
|
50 |
+
dagcomponentfuncs.DBC_Badge = function (props) {
|
51 |
+
const { value, colorMap } = props;
|
52 |
+
|
53 |
+
const parsedColorMap =
|
54 |
+
typeof colorMap === "string" ? JSON.parse(colorMap) : colorMap;
|
55 |
+
const bgColor = parsedColorMap?.[value] || "#000000";
|
56 |
+
|
57 |
+
console.log("Badge props:", props); // Debugging step
|
58 |
+
|
59 |
+
return React.createElement(
|
60 |
+
window.dash_bootstrap_components.Badge,
|
61 |
+
{
|
62 |
+
style: {
|
63 |
+
cssText: `background-color: ${bgColor} !important; color: #00000;`,
|
64 |
+
},
|
65 |
+
},
|
66 |
+
value,
|
67 |
+
);
|
68 |
+
};
|
assets/logo.svg
ADDED
|
requirements.in
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file is only used if you don't have hatch installed.
|
2 |
+
gunicorn
|
3 |
+
openpyxl
|
4 |
+
vizro==0.1.28
|
requirements.txt
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file was autogenerated by uv via the following command:
|
2 |
+
# uv pip compile requirements.in -o requirements.txt
|
3 |
+
annotated-types==0.7.0
|
4 |
+
# via pydantic
|
5 |
+
autoflake==2.3.1
|
6 |
+
# via vizro
|
7 |
+
black==24.4.2
|
8 |
+
# via vizro
|
9 |
+
blinker==1.8.2
|
10 |
+
# via flask
|
11 |
+
cachelib==0.9.0
|
12 |
+
# via flask-caching
|
13 |
+
certifi==2024.8.30
|
14 |
+
# via requests
|
15 |
+
charset-normalizer==3.4.0
|
16 |
+
# via requests
|
17 |
+
click==8.1.7
|
18 |
+
# via
|
19 |
+
# black
|
20 |
+
# flask
|
21 |
+
dash==2.18.1
|
22 |
+
# via
|
23 |
+
# dash-ag-grid
|
24 |
+
# dash-bootstrap-components
|
25 |
+
# vizro
|
26 |
+
dash-ag-grid==31.2.0
|
27 |
+
# via vizro
|
28 |
+
dash-bootstrap-components==1.6.0
|
29 |
+
# via vizro
|
30 |
+
dash-core-components==2.0.0
|
31 |
+
# via dash
|
32 |
+
dash-html-components==2.0.0
|
33 |
+
# via dash
|
34 |
+
dash-mantine-components==0.12.1
|
35 |
+
# via vizro
|
36 |
+
dash-table==5.0.0
|
37 |
+
# via dash
|
38 |
+
et-xmlfile==2.0.0
|
39 |
+
# via openpyxl
|
40 |
+
flask==3.0.3
|
41 |
+
# via
|
42 |
+
# dash
|
43 |
+
# flask-caching
|
44 |
+
flask-caching==2.3.0
|
45 |
+
# via vizro
|
46 |
+
gunicorn==23.0.0
|
47 |
+
# via -r requirements.in
|
48 |
+
idna==3.10
|
49 |
+
# via requests
|
50 |
+
importlib-metadata==8.5.0
|
51 |
+
# via
|
52 |
+
# dash
|
53 |
+
# flask
|
54 |
+
itsdangerous==2.2.0
|
55 |
+
# via flask
|
56 |
+
jinja2==3.1.4
|
57 |
+
# via flask
|
58 |
+
markupsafe==3.0.2
|
59 |
+
# via
|
60 |
+
# jinja2
|
61 |
+
# werkzeug
|
62 |
+
mypy-extensions==1.0.0
|
63 |
+
# via black
|
64 |
+
nest-asyncio==1.6.0
|
65 |
+
# via dash
|
66 |
+
numpy==2.0.2
|
67 |
+
# via pandas
|
68 |
+
openpyxl==3.1.5
|
69 |
+
# via -r requirements.in
|
70 |
+
packaging==24.1
|
71 |
+
# via
|
72 |
+
# black
|
73 |
+
# gunicorn
|
74 |
+
# plotly
|
75 |
+
pandas==2.2.3
|
76 |
+
# via vizro
|
77 |
+
pathspec==0.12.1
|
78 |
+
# via black
|
79 |
+
platformdirs==4.2.2
|
80 |
+
# via black
|
81 |
+
plotly==5.24.1
|
82 |
+
# via
|
83 |
+
# dash
|
84 |
+
# vizro
|
85 |
+
pydantic==2.9.2
|
86 |
+
# via vizro
|
87 |
+
pydantic-core==2.23.4
|
88 |
+
# via pydantic
|
89 |
+
pyflakes==3.2.0
|
90 |
+
# via autoflake
|
91 |
+
python-dateutil==2.9.0.post0
|
92 |
+
# via pandas
|
93 |
+
pytz==2024.2
|
94 |
+
# via pandas
|
95 |
+
requests==2.32.3
|
96 |
+
# via dash
|
97 |
+
retrying==1.3.4
|
98 |
+
# via dash
|
99 |
+
setuptools==75.3.0
|
100 |
+
# via dash
|
101 |
+
six==1.16.0
|
102 |
+
# via
|
103 |
+
# python-dateutil
|
104 |
+
# retrying
|
105 |
+
tenacity==9.0.0
|
106 |
+
# via plotly
|
107 |
+
tomli==2.1.0
|
108 |
+
# via
|
109 |
+
# autoflake
|
110 |
+
# black
|
111 |
+
typing-extensions==4.12.2
|
112 |
+
# via
|
113 |
+
# black
|
114 |
+
# dash
|
115 |
+
# pydantic
|
116 |
+
# pydantic-core
|
117 |
+
tzdata==2024.2
|
118 |
+
# via pandas
|
119 |
+
urllib3==2.2.3
|
120 |
+
# via requests
|
121 |
+
vizro==0.1.28
|
122 |
+
# via -r requirements.in
|
123 |
+
werkzeug==3.0.6
|
124 |
+
# via
|
125 |
+
# dash
|
126 |
+
# flask
|
127 |
+
wrapt==1.16.0
|
128 |
+
# via vizro
|
129 |
+
zipp==3.20.2
|
130 |
+
# via importlib-metadata
|
utils/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
"""Utils folder to contain helper functions and custom charts/components."""
|
utils/_helper.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Contains helper functions and configuration for AgGrid."""
|
2 |
+
|
3 |
+
import json
|
4 |
+
|
5 |
+
|
6 |
+
def convert_price_to_numeric(price):
|
7 |
+
if price == "Free To Play":
|
8 |
+
return 0.0
|
9 |
+
try:
|
10 |
+
return float(price.replace("£", ""))
|
11 |
+
except ValueError:
|
12 |
+
return None
|
13 |
+
|
14 |
+
|
15 |
+
def categorize_price(price):
|
16 |
+
if price == "Free To Play":
|
17 |
+
return price
|
18 |
+
|
19 |
+
try:
|
20 |
+
price_value = float(price.replace("£", ""))
|
21 |
+
except ValueError:
|
22 |
+
return "Unknown"
|
23 |
+
|
24 |
+
if price_value < 15:
|
25 |
+
return "Less than £15"
|
26 |
+
elif 15 <= price_value <= 30:
|
27 |
+
return "£15-£30"
|
28 |
+
elif 30 < price_value <= 45:
|
29 |
+
return "£30-£45"
|
30 |
+
else:
|
31 |
+
return "More than 45£"
|
32 |
+
|
33 |
+
|
34 |
+
COLOR_MAP = {
|
35 |
+
"Free To Play": "#afe7f9",
|
36 |
+
"Less than £15": "#3bbef1",
|
37 |
+
"£15-£30": "#0d8ed1",
|
38 |
+
"£30-£45": "#0061a4",
|
39 |
+
"More than 45£": "#003875",
|
40 |
+
}
|
41 |
+
|
42 |
+
|
43 |
+
columnDefs = [
|
44 |
+
{"field": "Rank", "cellDataType": "text", "flex": 1},
|
45 |
+
{"headerName": "Thumbnail", "field": "Thumbnail URL", "cellRenderer": "ImgThumbnail", "width": 150, "flex": 3},
|
46 |
+
{"field": "Name", "headerName": "Game", "cellDataType": "text", "flex": 3},
|
47 |
+
{
|
48 |
+
"field": "Current Players",
|
49 |
+
"cellDataType": "number",
|
50 |
+
"valueFormatter": {
|
51 |
+
"function": "d3.format(',.2r')(params.value)",
|
52 |
+
},
|
53 |
+
"flex": 3,
|
54 |
+
},
|
55 |
+
{
|
56 |
+
"field": "Peak Today",
|
57 |
+
"cellDataType": "number",
|
58 |
+
"valueFormatter": {"function": "d3.format(',.2r')(params.value)"},
|
59 |
+
"flex": 3,
|
60 |
+
},
|
61 |
+
{
|
62 |
+
"field": "Price Category",
|
63 |
+
"cellDataType": "text",
|
64 |
+
"cellRenderer": "DBC_Badge",
|
65 |
+
"cellRendererParams": {"colorMap": json.dumps(COLOR_MAP)},
|
66 |
+
"flex": 2,
|
67 |
+
},
|
68 |
+
{"field": "Store Link", "cellRenderer": "DBC_Button", "width": 200, "flex": 2},
|
69 |
+
]
|