li-nguyen commited on
Commit
a760588
·
1 Parent(s): b759361

Add app configuration

Browse files
.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: Figure Friday Week 5
3
- emoji: 📚
4
- colorFrom: indigo
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  license: apache-2.0
9
- short_description: My app for Figure Friday - Week 5 2025
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ]