.gitattributes CHANGED
@@ -34,4 +34,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/images/kpi_dashboard.gif filter=lfs diff=lfs merge=lfs -text
37
- kpi-dashboard.gif filter=lfs diff=lfs merge=lfs -text
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/images/kpi_dashboard.gif filter=lfs diff=lfs merge=lfs -text
 
.gitignore DELETED
@@ -1,144 +0,0 @@
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/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: KPI Dashboard
3
  emoji: 📊
4
  colorFrom: blue
5
  colorTo: blue
@@ -9,43 +9,28 @@ license: apache-2.0
9
  short_description: Example of a Key Performance Indicator (KPI) dashboard
10
  ---
11
 
12
- # KPI Dashboard
13
 
14
- This dashboard provides an example of a Key Performance Indicator (KPI) dashboard, designed to help users get started
15
- and extend further. It uses fictional budget data to demonstrate the capabilities of Vizro using real world applications.
16
 
17
- **Created by:** [Huong Li Nguyen](https://github.com/huong-li-nguyen)
18
-
19
- ---
20
-
21
- ### 🗓️ Data
22
-
23
- Special thanks to the [#RWFD Real World Fake Data initiative](https://data.world/markbradbourne/rwfd-real-world-fake-data), a community project that
24
  provides high-quality fake data for creating realistic dashboard examples for real-world applications.
25
 
26
- **Note:** The data has been additionally edited for the purpose of this example.
27
-
28
- ### 📊 Plotly resources
29
-
30
- - [Bar charts](https://plotly.com/python/bar-charts/)
31
- - [Pie charts](https://plotly.com/python/pie-charts/)
32
- - [Choropleth maps](https://plotly.com/python/choropleth-maps/)
33
- - [Unstacked area charts](https://plotly.com/python/filled-area-plots/)
34
 
35
- ### 🚀 Vizro features applied
36
-
37
- - [Vizro tutorial on pages, layouts and dashboards](https://vizro.readthedocs.io/en/stable/pages/tutorials/explore-components/)
38
- - [Custom components](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-components/)
39
- - [Custom charts](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-charts/)
40
- - [Custom CSS](https://vizro.readthedocs.io/en/stable/pages/user-guides/assets/)
41
-
42
- ### 🖥️ App demo
43
-
44
- <img src="./images/kpi-dashboard.gif" alt="Gif to KPI dashboard" width="600">
45
-
46
- ---
47
 
48
- ## How to run the example locally
49
 
50
- 1. Run the `app.py` file with your environment activated where `vizro` is installed.
51
- 2. You should now be able to access the app locally via http://127.0.0.1:8050/.
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: KPI Demo
3
  emoji: 📊
4
  colorFrom: blue
5
  colorTo: blue
 
9
  short_description: Example of a Key Performance Indicator (KPI) dashboard
10
  ---
11
 
12
+ # KPI dashboard
13
 
14
+ This demo dashboard provides an example of a Key Performance Indicator (KPI) dashboard, designed to help users get started and extend further.
15
+ It uses fictional budget data to demonstrate the capabilities of Vizro using real world applications.
16
 
17
+ Special thanks to the [#RWFD Real World Fake Data initiative](https://opendatainitiative.io/), a community project that
 
 
 
 
 
 
18
  provides high-quality fake data for creating realistic dashboard examples for real-world applications.
19
 
20
+ Note: The data has been additionally edited for the purpose of this example.
 
 
 
 
 
 
 
21
 
22
+ <img src="./assets/images/kpi_dashboard.gif" alt="Gif to KPI dashboard">
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ ## Possible future iterations
25
 
26
+ - Enable selection of year filter
27
+ - Enable current year vs. past year comparison
28
+ - Enable dynamic KPI Cards
29
+ - Bar - Enable drill-downs from Issue to Sub-issue and Product to Sub-product
30
+ - Bar - Reformat numbers with commas in bar chart
31
+ - Bar - Left-align y-axis labels
32
+ - Bar - Shorten labels
33
+ - Line - Customize function to always show selected year vs. past year
34
+ - Table-view - Check why date format does not work on `Date Received`
35
+ - Table-view - Add icons to `On time?` column
36
+ - Table-view - Improve speed by applying cache or overcome limitation that entire data set is loaded in
app.py CHANGED
@@ -1,131 +1,114 @@
1
  """Example to show dashboard configuration."""
2
 
3
- from dash import html, get_asset_url
4
- import dash_bootstrap_components as dbc
5
  import pandas as pd
6
  import vizro.models as vm
7
- from utils._charts import COLUMN_DEFS, FlexContainer, area, bar, choropleth, pie
8
- from utils._helper import clean_data_and_add_columns, create_data_for_kpi_cards
9
  from vizro import Vizro
10
  from vizro.actions import filter_interaction
11
- from vizro.figures import kpi_card_reference
12
  from vizro.tables import dash_ag_grid
13
 
14
  # DATA --------------------------------------------------------------------------------------------
15
  df_complaints = pd.read_csv("https://query.data.world/s/glbdstahsuw3hjgunz3zssggk7dsfu?dws=00000")
16
  df_complaints = clean_data_and_add_columns(df_complaints)
17
- df_kpi_cards = create_data_for_kpi_cards(df_complaints)
18
- vm.Page.add_type("components", FlexContainer)
19
-
20
 
21
  # SUB-SECTIONS ------------------------------------------------------------------------------------
22
- kpi_banner = FlexContainer(
 
 
23
  components=[
24
- vm.Figure(
25
- id="kpi-reverse-coloring",
26
- figure=kpi_card_reference(
27
- df_kpi_cards,
28
- value_column="Total Complaints_2019",
29
- reference_column="Total Complaints_2018",
30
- title="Total Complaints",
31
- value_format="{value:.0f}",
32
- reference_format="{delta_relative:+.1%} vs. 2018 ({reference:.0f})",
33
- icon="person",
34
- ),
35
  ),
36
- vm.Figure(
37
- figure=kpi_card_reference(
38
- df_kpi_cards,
39
- value_column="Closed Complaints_2019",
40
- reference_column="Closed Complaints_2018",
41
- title="Closed Complaints",
42
- value_format="{value:.1f}%",
43
- reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
44
- icon="inventory",
45
- )
46
  ),
47
- vm.Figure(
48
- figure=kpi_card_reference(
49
- df_kpi_cards,
50
- value_column="Timely response_2019",
51
- reference_column="Timely response_2018",
52
- title="Timely Response",
53
- value_format="{value:.1f}%",
54
- reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
55
- icon="timer",
56
- )
57
  ),
58
- vm.Figure(
59
- figure=kpi_card_reference(
60
- df_kpi_cards,
61
- value_column="Closed w/o cost_2019",
62
- reference_column="Closed w/o cost_2018",
63
- title="Closed w/o cost",
64
- value_format="{value:.1f}%",
65
- reference_format="{delta:.1f}pp vs. 2018 ({reference:.1f}%)",
66
- icon="payments",
67
- )
68
  ),
69
- vm.Figure(
70
- figure=kpi_card_reference(
71
- df_kpi_cards,
72
- value_column="Consumer disputed_2019",
73
- reference_column="Consumer disputed_2018",
74
- title="Consumer disputed",
75
- value_format="{value:.1f}%",
76
- reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
77
- icon="sentiment_dissatisfied",
78
- )
 
 
 
79
  ),
80
  ],
81
- classname="kpi-banner",
82
  )
83
 
84
  bar_charts_tabbed = vm.Tabs(
85
  tabs=[
86
  vm.Container(
87
- title="By Product",
88
  components=[
89
  vm.Graph(
90
  figure=bar(
91
  data_frame=df_complaints,
92
- y="Product",
93
  x="Complaint ID",
94
  ),
95
  )
96
  ],
97
  ),
98
  vm.Container(
99
- title="By Channel",
100
  components=[
101
  vm.Graph(
102
  figure=bar(
103
  data_frame=df_complaints,
104
- y="Channel",
105
  x="Complaint ID",
106
  ),
107
  )
108
  ],
109
  ),
110
  vm.Container(
111
- title="By Region",
112
  components=[
113
  vm.Graph(
114
  figure=bar(
115
  data_frame=df_complaints,
116
- y="Region",
117
  x="Complaint ID",
118
  ),
119
  )
120
  ],
121
  ),
122
  vm.Container(
123
- title="By Issue",
124
  components=[
125
  vm.Graph(
126
  figure=bar(
127
  data_frame=df_complaints,
128
- y="Issue",
129
  x="Complaint ID",
130
  ),
131
  )
@@ -139,12 +122,9 @@ page_exec = vm.Page(
139
  title="Executive View",
140
  layout=vm.Layout(
141
  grid=[
142
- [0, 0],
143
  [0, 0],
144
  [1, 2],
145
  [1, 2],
146
- [1, 2],
147
- [1, 3],
148
  [1, 3],
149
  [1, 3],
150
  ],
@@ -152,10 +132,17 @@ page_exec = vm.Page(
152
  components=[
153
  kpi_banner,
154
  bar_charts_tabbed,
155
- vm.Graph(figure=area(data_frame=df_complaints, y="Complaint ID", x="Month")),
156
  vm.Graph(
157
  figure=pie(
158
  data_frame=df_complaints[df_complaints["Company response - Closed"] != "Not closed"],
 
 
 
 
 
 
 
159
  values="Complaint ID",
160
  names="Company response - Closed",
161
  title="Closed company responses",
@@ -166,15 +153,23 @@ page_exec = vm.Page(
166
 
167
  page_region = vm.Page(
168
  title="Regional View",
169
- layout=vm.Layout(grid=[[0, 1]]),
170
  components=[
 
 
 
 
 
 
 
 
 
171
  vm.Graph(
172
  figure=choropleth(
173
  data_frame=df_complaints,
174
  locations="State",
175
  color="Complaint ID",
176
- title="Complaints by State <br><sup> ⤵ Click on a state to filter the "
177
- "charts on the right. Refresh the page to deselect.</sup>",
178
  custom_data=["State"],
179
  ),
180
  actions=[
@@ -186,26 +181,26 @@ page_region = vm.Page(
186
  vm.Tabs(
187
  tabs=[
188
  vm.Container(
189
- title="By Product",
190
  components=[
191
  vm.Graph(
192
- id="regional-product",
193
  figure=bar(
194
  data_frame=df_complaints,
195
- y="Product",
196
  x="Complaint ID",
197
  ),
198
  )
199
  ],
200
  ),
201
  vm.Container(
202
- title="By Issue",
203
  components=[
204
  vm.Graph(
205
- id="regional-issue",
206
  figure=bar(
207
  data_frame=df_complaints,
208
- y="Issue",
209
  x="Complaint ID",
210
  ),
211
  )
@@ -237,7 +232,7 @@ page_table = vm.Page(
237
 
238
  dashboard = vm.Dashboard(
239
  pages=[page_exec, page_region, page_table],
240
- title="Cumulus Financial Corp. - Fiscal Year 2019",
241
  navigation=vm.Navigation(
242
  nav_selector=vm.NavBar(
243
  items=[
@@ -250,16 +245,7 @@ dashboard = vm.Dashboard(
250
  )
251
 
252
  app = Vizro().build(dashboard)
253
- app.dash.layout.children.append(
254
- dbc.NavLink(
255
- ["Made with ", html.Img(src=get_asset_url("images/logo.svg"), id="banner", alt="Vizro logo"), "vizro"],
256
- href="https://github.com/mckinsey/vizro",
257
- target="_blank",
258
- external_link=True,
259
- className="anchor-container",
260
- )
261
- )
262
  server = app.dash.server
263
 
264
- if __name__ == "__main__":
265
- app.run()
 
1
  """Example to show dashboard configuration."""
2
 
 
 
3
  import pandas as pd
4
  import vizro.models as vm
5
+ from utils._charts import COLUMN_DEFS, KPI, bar, choropleth, line, pie
6
+ from utils._helper import clean_data_and_add_columns
7
  from vizro import Vizro
8
  from vizro.actions import filter_interaction
 
9
  from vizro.tables import dash_ag_grid
10
 
11
  # DATA --------------------------------------------------------------------------------------------
12
  df_complaints = pd.read_csv("https://query.data.world/s/glbdstahsuw3hjgunz3zssggk7dsfu?dws=00000")
13
  df_complaints = clean_data_and_add_columns(df_complaints)
14
+ vm.Container.add_type("components", KPI)
 
 
15
 
16
  # SUB-SECTIONS ------------------------------------------------------------------------------------
17
+ kpi_banner = vm.Container(
18
+ id="kpi-banner",
19
+ title="",
20
  components=[
21
+ # Note: For some KPIs the icon/sign go in opposite directions as an increase e.g. in complaints is negative
22
+ KPI(
23
+ title="Total Complaints",
24
+ value="75.513",
25
+ icon="arrow_circle_up",
26
+ sign="delta-neg",
27
+ ref_value="6.8% vs. LY",
 
 
 
 
28
  ),
29
+ KPI(
30
+ title="Closed Complaints",
31
+ value="99.6%",
32
+ icon="arrow_circle_up",
33
+ sign="delta-pos",
34
+ ref_value="+0.2% vs. LY",
 
 
 
 
35
  ),
36
+ KPI(
37
+ title="Open Complaints",
38
+ value="0.4%",
39
+ icon="arrow_circle_down",
40
+ sign="delta-pos",
41
+ ref_value="-0.2% vs. LY",
 
 
 
 
42
  ),
43
+ KPI(
44
+ title="Timely Response",
45
+ value="98.1%",
46
+ icon="arrow_circle_up",
47
+ sign="delta-pos",
48
+ ref_value="+10.5% vs. LY",
 
 
 
 
49
  ),
50
+ KPI(
51
+ title="Closed w/o cost",
52
+ value="84.5%",
53
+ icon="arrow_circle_down",
54
+ sign="delta-neg",
55
+ ref_value="-8.5% vs. LY",
56
+ ),
57
+ KPI(
58
+ title="Consumer disputed",
59
+ value="9.5%",
60
+ icon="arrow_circle_up",
61
+ sign="delta-neg",
62
+ ref_value="+2.3% vs. LY",
63
  ),
64
  ],
 
65
  )
66
 
67
  bar_charts_tabbed = vm.Tabs(
68
  tabs=[
69
  vm.Container(
70
+ title="By Issue",
71
  components=[
72
  vm.Graph(
73
  figure=bar(
74
  data_frame=df_complaints,
75
+ y="Issue",
76
  x="Complaint ID",
77
  ),
78
  )
79
  ],
80
  ),
81
  vm.Container(
82
+ title="By Product",
83
  components=[
84
  vm.Graph(
85
  figure=bar(
86
  data_frame=df_complaints,
87
+ y="Product",
88
  x="Complaint ID",
89
  ),
90
  )
91
  ],
92
  ),
93
  vm.Container(
94
+ title="By Channel",
95
  components=[
96
  vm.Graph(
97
  figure=bar(
98
  data_frame=df_complaints,
99
+ y="Channel",
100
  x="Complaint ID",
101
  ),
102
  )
103
  ],
104
  ),
105
  vm.Container(
106
+ title="By Region",
107
  components=[
108
  vm.Graph(
109
  figure=bar(
110
  data_frame=df_complaints,
111
+ y="Region",
112
  x="Complaint ID",
113
  ),
114
  )
 
122
  title="Executive View",
123
  layout=vm.Layout(
124
  grid=[
 
125
  [0, 0],
126
  [1, 2],
127
  [1, 2],
 
 
128
  [1, 3],
129
  [1, 3],
130
  ],
 
132
  components=[
133
  kpi_banner,
134
  bar_charts_tabbed,
135
+ vm.Graph(figure=line(data_frame=df_complaints, y="Complaint ID", x="Year-Month Received")),
136
  vm.Graph(
137
  figure=pie(
138
  data_frame=df_complaints[df_complaints["Company response - Closed"] != "Not closed"],
139
+ custom_order=[
140
+ "Closed with explanation",
141
+ "Closed without relief",
142
+ "Closed with non-monetary relief",
143
+ "Closed with relief",
144
+ "Closed with monetary relief",
145
+ ],
146
  values="Complaint ID",
147
  names="Company response - Closed",
148
  title="Closed company responses",
 
153
 
154
  page_region = vm.Page(
155
  title="Regional View",
156
+ layout=vm.Layout(grid=[[0, 0]] + [[1, 2]] * 4),
157
  components=[
158
+ vm.Card(
159
+ text="""
160
+ ##### Click on a state inside the map to filter the bar charts on the right.
161
+
162
+ - Which state has the most complaints?
163
+ - What are the three biggest issues in California?
164
+ - What is the product with the most complaints in Texas?
165
+ """
166
+ ),
167
  vm.Graph(
168
  figure=choropleth(
169
  data_frame=df_complaints,
170
  locations="State",
171
  color="Complaint ID",
172
+ title="Complaints by State",
 
173
  custom_data=["State"],
174
  ),
175
  actions=[
 
181
  vm.Tabs(
182
  tabs=[
183
  vm.Container(
184
+ title="By Issue",
185
  components=[
186
  vm.Graph(
187
+ id="regional-issue",
188
  figure=bar(
189
  data_frame=df_complaints,
190
+ y="Issue",
191
  x="Complaint ID",
192
  ),
193
  )
194
  ],
195
  ),
196
  vm.Container(
197
+ title="By Product",
198
  components=[
199
  vm.Graph(
200
+ id="regional-product",
201
  figure=bar(
202
  data_frame=df_complaints,
203
+ y="Product",
204
  x="Complaint ID",
205
  ),
206
  )
 
232
 
233
  dashboard = vm.Dashboard(
234
  pages=[page_exec, page_region, page_table],
235
+ title="Cumulus Financial Corporation",
236
  navigation=vm.Navigation(
237
  nav_selector=vm.NavBar(
238
  items=[
 
245
  )
246
 
247
  app = Vizro().build(dashboard)
 
 
 
 
 
 
 
 
 
248
  server = app.dash.server
249
 
250
+ if __name__ == "__main__":
251
+ app.run()
assets/css/custom.css CHANGED
@@ -2,64 +2,78 @@
2
  padding-left: 4px;
3
  }
4
 
5
- .card-kpi {
6
- min-width: 220px;
7
- padding: 0.75rem;
8
  }
9
 
10
- .kpi-banner {
11
- display: flex;
12
- gap: 1rem;
13
- height: 100%;
14
- overflow: scroll;
15
  }
16
 
17
- .kpi-banner .figure-container {
18
- height: unset;
19
  }
20
 
21
- .kpi-banner::-webkit-scrollbar-thumb {
22
- border: 5px solid var(--main-container-bg-color);
 
 
23
  }
24
 
25
- /* Apply reverse color coding for one KPI card */
26
- #kpi-reverse-coloring .card-kpi .color-pos.card-footer {
27
- color: var(--bs-pink);
28
  }
29
 
30
- #kpi-reverse-coloring .card-kpi .color-neg.card-footer {
31
- color: var(--bs-blue);
32
  }
33
 
34
- #kpi-reverse-coloring .card-kpi:has(.color-pos) {
35
- border-left: 4px solid var(--bs-pink);
 
36
  }
37
 
38
- #kpi-reverse-coloring .card-kpi:has(.color-neg) {
39
- border-left: 4px solid var(--bs-blue);
 
 
 
40
  }
41
 
42
- .anchor-container {
43
  align-items: center;
44
- background: var(--text-primary);
45
- border-top-left-radius: 8px;
46
- bottom: 0;
47
- color: var(--text-contrast-primary);
48
  display: flex;
49
- font-size: 0.8rem;
50
- font-weight: 500;
51
- height: 24px;
52
- padding: 0 12px;
53
- position: fixed;
54
- right: 0;
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
- .anchor-container:focus,
58
- .anchor-container:hover {
59
- background: var(--text-secondary);
60
- color: var(--text-contrast-primary);
 
 
 
 
61
  }
62
 
63
- img#banner {
64
- height: 16px;
 
65
  }
 
2
  padding-left: 4px;
3
  }
4
 
5
+ .card {
6
+ padding: 8px;
 
7
  }
8
 
9
+ .kpi-card-ref {
10
+ min-width: 168px;
 
 
 
11
  }
12
 
13
+ .kpi-card-ref h4 {
14
+ margin: 0;
15
  }
16
 
17
+ .kpi-card-ref > span {
18
+ display: flex;
19
+ font-weight: 600;
20
+ gap: 4px;
21
  }
22
 
23
+ .kpi-card-ref .delta-pos {
24
+ color: #1a85ff;
 
25
  }
26
 
27
+ .kpi-card-ref .delta-neg {
28
+ color: #d41159;
29
  }
30
 
31
+ .kpi-card-ref .material-symbols-outlined {
32
+ font-size: 16px;
33
+ line-height: 20px;
34
  }
35
 
36
+ .kpi-card-ref > div {
37
+ display: flex;
38
+ flex-direction: row;
39
+ gap: 8px;
40
+ margin: 0;
41
  }
42
 
43
+ .kpi-card-ref > p {
44
  align-items: center;
45
+ color: var(--text-secondary);
 
 
 
46
  display: flex;
47
+ flex-grow: 1;
48
+ font-size: 1rem;
49
+ font-size: 3.6vh;
50
+ font-weight: 600;
51
+ line-height: unset;
52
+ }
53
+
54
+ .kpi-card-ref:has(.delta-pos) {
55
+ border-left: 4px solid #1a85ff;
56
+ }
57
+
58
+ .kpi-card-ref:has(.delta-neg) {
59
+ border-left: 4px solid #d41159;
60
+ }
61
+
62
+ .card ul {
63
+ margin-bottom: 0;
64
  }
65
 
66
+ #kpi-banner .container__title {
67
+ display: none;
68
+ }
69
+
70
+ #kpi-banner .grid-layout {
71
+ display: flex;
72
+ flex-direction: row;
73
+ overflow: auto;
74
  }
75
 
76
+ #kpi-banner > ::-webkit-scrollbar-thumb {
77
+ border: 6px solid;
78
+ border-color: var(--main-container-bg-color);
79
  }
images/kpi-dashboard.gif → assets/images/kpi_dashboard.gif RENAMED
File without changes
requirements.txt CHANGED
@@ -6,7 +6,7 @@ blinker==1.8.2
6
  # via flask
7
  cachelib==0.9.0
8
  # via flask-caching
9
- certifi==2024.8.30
10
  # via requests
11
  charset-normalizer==3.3.2
12
  # via requests
@@ -37,7 +37,7 @@ flask-caching==2.3.0
37
  # via vizro
38
  gunicorn==23.0.0
39
  # via -r requirements.in
40
- idna==3.8
41
  # via requests
42
  importlib-metadata==8.4.0
43
  # via dash
@@ -52,14 +52,16 @@ markupsafe==2.1.5
52
  nest-asyncio==1.6.0
53
  # via dash
54
  numpy==2.1.0
55
- # via pandas
 
 
56
  packaging==24.1
57
  # via
58
  # gunicorn
59
  # plotly
60
  pandas==2.2.2
61
  # via vizro
62
- plotly==5.24.0
63
  # via dash
64
  pydantic==2.8.2
65
  # via vizro
@@ -73,10 +75,12 @@ requests==2.32.3
73
  # via dash
74
  retrying==1.3.4
75
  # via dash
76
- ruff==0.6.3
77
  # via vizro
78
- setuptools==74.0.0
79
- # via dash
 
 
80
  six==1.16.0
81
  # via
82
  # python-dateutil
@@ -92,13 +96,14 @@ tzdata==2024.1
92
  # via pandas
93
  urllib3==2.2.2
94
  # via requests
95
- vizro==0.1.21
96
  # via -r requirements.in
97
  werkzeug==3.0.4
98
  # via
99
  # dash
100
  # flask
 
101
  wrapt==1.16.0
102
  # via vizro
103
- zipp==3.20.1
104
  # via importlib-metadata
 
6
  # via flask
7
  cachelib==0.9.0
8
  # via flask-caching
9
+ certifi==2024.7.4
10
  # via requests
11
  charset-normalizer==3.3.2
12
  # via requests
 
37
  # via vizro
38
  gunicorn==23.0.0
39
  # via -r requirements.in
40
+ idna==3.7
41
  # via requests
42
  importlib-metadata==8.4.0
43
  # via dash
 
52
  nest-asyncio==1.6.0
53
  # via dash
54
  numpy==2.1.0
55
+ # via
56
+ # pandas
57
+ # vizro
58
  packaging==24.1
59
  # via
60
  # gunicorn
61
  # plotly
62
  pandas==2.2.2
63
  # via vizro
64
+ plotly==5.23.0
65
  # via dash
66
  pydantic==2.8.2
67
  # via vizro
 
75
  # via dash
76
  retrying==1.3.4
77
  # via dash
78
+ ruff==0.6.1
79
  # via vizro
80
+ setuptools==73.0.1
81
+ # via
82
+ # dash
83
+ # vizro
84
  six==1.16.0
85
  # via
86
  # python-dateutil
 
96
  # via pandas
97
  urllib3==2.2.2
98
  # via requests
99
+ vizro==0.1.20
100
  # via -r requirements.in
101
  werkzeug==3.0.4
102
  # via
103
  # dash
104
  # flask
105
+ # vizro
106
  wrapt==1.16.0
107
  # via vizro
108
+ zipp==3.20.0
109
  # via importlib-metadata
utils/__init__.py CHANGED
@@ -1 +1 @@
1
- """Utils folder to contain helper functions and custom charts/components."""
 
1
+ """Utils folder to contain helper functions and custom charts/components."""
utils/_charts.py CHANGED
@@ -2,8 +2,8 @@
2
 
3
  from typing import List, Literal, Optional
4
 
 
5
  import pandas as pd
6
- import plotly.graph_objects as go
7
  import vizro.models as vm
8
  import vizro.plotly.express as px
9
  from dash import html
@@ -11,17 +11,32 @@ from vizro.models.types import capture
11
 
12
 
13
  # CUSTOM COMPONENTS -------------------------------------------------------------
14
- class FlexContainer(vm.Container):
15
- """Custom flex `Container`."""
16
-
17
- type: Literal["flex_container"] = "flex_container"
18
- title: str = None # Title exists in vm.Container but we don't want to use it here.
19
- classname: str = "d-flex"
 
 
 
 
 
20
 
21
  def build(self):
22
- """Returns a flex container."""
23
- return html.Div(
24
- id=self.id, children=[component.build() for component in self.components], className=self.classname
 
 
 
 
 
 
 
 
 
 
25
  )
26
 
27
 
@@ -34,11 +49,8 @@ def bar(
34
  top_n: int = 15,
35
  custom_data: Optional[List[str]] = None,
36
  ):
37
- """Custom bar chart implementation.
38
-
39
- Based on [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar).
40
- """
41
  df_agg = data_frame.groupby(y).agg({x: "count"}).sort_values(by=x, ascending=False).reset_index()
 
42
  fig = px.bar(
43
  data_frame=df_agg.head(top_n),
44
  x=x,
@@ -48,38 +60,21 @@ def bar(
48
  color_discrete_sequence=["#1A85FF"],
49
  custom_data=custom_data,
50
  )
51
- fig.update_layout(xaxis_title="# of Complaints", yaxis={"title": "", "autorange": "reversed"})
52
  return fig
53
 
54
 
55
  @capture("graph")
56
- def area(x: str, y: str, data_frame: pd.DataFrame):
57
- """Custom chart to create unstacked area chart.
58
-
59
- Based on [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html).
60
-
61
- """
62
- df_agg = data_frame.groupby(["Year", "Month"]).agg({y: "count"}).reset_index()
63
- df_agg_2019 = df_agg[df_agg["Year"] == "2018"]
64
- df_agg_2020 = df_agg[df_agg["Year"] == "2019"]
65
-
66
- fig = go.Figure()
67
- fig.add_trace(
68
- go.Scatter(x=df_agg_2020[x], y=df_agg_2020[y], fill="tozeroy", name="2019", marker={"color": "#1a85ff"})
69
- )
70
- fig.add_trace(go.Scatter(x=df_agg_2019[x], y=df_agg_2019[y], fill="tonexty", name="2018", marker={"color": "grey"}))
71
- fig.update_layout(
72
  title="Complaints over time",
73
- xaxis_title="Date Received",
74
- yaxis_title="# of Complaints",
75
- title_pad_t=4,
76
- xaxis={
77
- "showgrid": False,
78
- "tickmode": "array",
79
- "tickvals": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
80
- "ticktext": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
81
- },
82
  )
 
83
  return fig
84
 
85
 
@@ -89,29 +84,25 @@ def pie(
89
  values: str,
90
  data_frame: pd.DataFrame = None,
91
  title: Optional[str] = None,
 
92
  ):
93
- """Custom pie chart implementation.
94
-
95
- Based on [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie).
96
- """
97
  df_agg = data_frame.groupby(names).agg({values: "count"}).reset_index()
 
 
 
 
 
98
  fig = px.pie(
99
- data_frame=df_agg,
100
  names=names,
101
  values=values,
102
- color=names,
103
- color_discrete_map={
104
- "Closed with explanation": "#1a85ff",
105
- "Closed with monetary relief": "#d41159",
106
- "Closed with non-monetary relief": "#adbedc",
107
- "Closed without relief": "#7ea1ee",
108
- "Closed with relief": "#df658c",
109
- "Closed": "#1a85ff",
110
- },
111
  title=title,
112
  hole=0.4,
113
  )
114
- fig.update_layout(legend_x=1, legend_y=1, title_pad_t=2, margin={"l": 0, "r": 0, "t": 60, "b": 0})
 
 
115
  return fig
116
 
117
 
@@ -123,11 +114,8 @@ def choropleth(
123
  title: Optional[str] = None,
124
  custom_data: Optional[List[str]] = None,
125
  ):
126
- """Custom choropleth implementation.
127
-
128
- Based on [px.choropleth](https://plotly.com/python-api-reference/generated/plotly.express.choropleth).
129
- """
130
  df_agg = data_frame.groupby(locations).agg({color: "count"}).reset_index()
 
131
  fig = px.choropleth(
132
  data_frame=df_agg,
133
  locations=locations,
@@ -151,7 +139,8 @@ def choropleth(
151
  title=title,
152
  custom_data=custom_data,
153
  )
154
- fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "bottom"}, "orientation": "h", "x": 0.5, "y": 0})
 
155
  return fig
156
 
157
 
@@ -201,4 +190,4 @@ COLUMN_DEFS = [
201
  "flex": 6,
202
  },
203
  {"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3},
204
- ]
 
2
 
3
  from typing import List, Literal, Optional
4
 
5
+ import dash_bootstrap_components as dbc
6
  import pandas as pd
 
7
  import vizro.models as vm
8
  import vizro.plotly.express as px
9
  from dash import html
 
11
 
12
 
13
  # CUSTOM COMPONENTS -------------------------------------------------------------
14
+ # Note: This is a static KPI Card only (it will not be reactive to controls). A new dynamic KPI Card component
15
+ # is currently in development.
16
+ class KPI(vm.VizroBaseModel):
17
+ """Static custom `KPI` Card."""
18
+
19
+ type: Literal["kpi"] = "kpi"
20
+ title: str
21
+ value: str
22
+ icon: str
23
+ sign: Literal["delta-pos", "delta-neg"]
24
+ ref_value: str
25
 
26
  def build(self):
27
+ return dbc.Card(
28
+ [
29
+ html.H4(self.title),
30
+ html.P(self.value),
31
+ html.Span(
32
+ [
33
+ html.Span(self.icon, className="material-symbols-outlined"),
34
+ html.Span(self.ref_value),
35
+ ],
36
+ className=self.sign,
37
+ ),
38
+ ],
39
+ className="kpi-card-ref",
40
  )
41
 
42
 
 
49
  top_n: int = 15,
50
  custom_data: Optional[List[str]] = None,
51
  ):
 
 
 
 
52
  df_agg = data_frame.groupby(y).agg({x: "count"}).sort_values(by=x, ascending=False).reset_index()
53
+
54
  fig = px.bar(
55
  data_frame=df_agg.head(top_n),
56
  x=x,
 
60
  color_discrete_sequence=["#1A85FF"],
61
  custom_data=custom_data,
62
  )
63
+ fig.update_layout(xaxis_title="# of Complaints", yaxis=dict(title="", autorange="reversed")) # noqa: C408
64
  return fig
65
 
66
 
67
  @capture("graph")
68
+ def line(x: str, y: str, data_frame: pd.DataFrame):
69
+ df_agg = data_frame.groupby(x).agg({y: "count"}).reset_index()
70
+ fig = px.area(
71
+ data_frame=df_agg,
72
+ x=x,
73
+ y=y,
74
+ color_discrete_sequence=["#1A85FF"],
 
 
 
 
 
 
 
 
 
75
  title="Complaints over time",
 
 
 
 
 
 
 
 
 
76
  )
77
+ fig.update_layout(xaxis_title="Date Received", yaxis_title="# of Complaints", title_pad_t=4)
78
  return fig
79
 
80
 
 
84
  values: str,
85
  data_frame: pd.DataFrame = None,
86
  title: Optional[str] = None,
87
+ custom_order: Optional[List[str]] = None,
88
  ):
 
 
 
 
89
  df_agg = data_frame.groupby(names).agg({values: "count"}).reset_index()
90
+
91
+ # Apply custom order so colors are applied correctly to the pie chart
92
+ order_mapping = {category: index for index, category in enumerate(custom_order)}
93
+ df_sorted = df_agg.sort_values(by=names, key=lambda names: names.map(order_mapping))
94
+
95
  fig = px.pie(
96
+ data_frame=df_sorted,
97
  names=names,
98
  values=values,
99
+ color_discrete_sequence=["#1a85ff", "#7ea1ee", "#adbedc", "#df658c", "#d41159"],
 
 
 
 
 
 
 
 
100
  title=title,
101
  hole=0.4,
102
  )
103
+
104
+ fig.update_layout(legend_x=1, legend_y=1, title_pad_t=2, margin=dict(l=0, r=0, t=60, b=0)) # noqa: C408
105
+ fig.update_traces(sort=False)
106
  return fig
107
 
108
 
 
114
  title: Optional[str] = None,
115
  custom_data: Optional[List[str]] = None,
116
  ):
 
 
 
 
117
  df_agg = data_frame.groupby(locations).agg({color: "count"}).reset_index()
118
+
119
  fig = px.choropleth(
120
  data_frame=df_agg,
121
  locations=locations,
 
139
  title=title,
140
  custom_data=custom_data,
141
  )
142
+
143
+ fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "right"}})
144
  return fig
145
 
146
 
 
190
  "flex": 6,
191
  },
192
  {"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3},
193
+ ]
utils/_helper.py CHANGED
@@ -1,7 +1,5 @@
1
  """Contains helper functions and variables."""
2
 
3
- from functools import reduce
4
-
5
  import numpy as np
6
  import pandas as pd
7
 
@@ -40,14 +38,13 @@ def clean_data_and_add_columns(data: pd.DataFrame):
40
  data["Company response - detailed"] = data["Company response - detailed"].replace("Closed", "Closed without relief")
41
  data["State"] = data["State"].replace("UNITED STATES MINOR OUTLYING ISLANDS", "UM")
42
  data["State"] = fill_na_with_random(data, "State")
43
- data["Consumer disputed?"] = data["Consumer disputed?"].fillna("No")
44
 
45
  # Convert to correct data type
46
  data["Date Received"] = pd.to_datetime(data["Date Received"], format="%m/%d/%y").dt.strftime("%Y-%m-%d")
 
47
 
48
  # Create additional columns
49
- data["Month"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%m")
50
- data["Year"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%Y")
51
  data["Region"] = data["State"].map(REGION_MAPPING)
52
  data["Company response"] = np.where(
53
  data["Company response - detailed"].str.contains("Closed"), "Closed", data["Company response - detailed"]
@@ -55,74 +52,4 @@ def clean_data_and_add_columns(data: pd.DataFrame):
55
  data["Company response - Closed"] = np.where(
56
  data["Company response - detailed"].str.contains("Closed"), data["Company response - detailed"], "Not closed"
57
  )
58
-
59
- # Filter 2018 and 2019 only
60
- data = data[(data["Year"].isin(["2018", "2019"]))]
61
- return data
62
-
63
-
64
- def create_data_for_kpi_cards(data):
65
- """Formats and aggregates the data for the KPI cards."""
66
- total_complaints = (
67
- data.groupby("Year")
68
- .agg({"Complaint ID": "count"})
69
- .rename(columns={"Complaint ID": "Total Complaints"})
70
- .reset_index()
71
- )
72
- closed_complaints = (
73
- data[data["Company response"] == "Closed"]
74
- .groupby("Year")
75
- .agg({"Complaint ID": "count"})
76
- .rename(columns={"Complaint ID": "Closed Complaints"})
77
- .reset_index()
78
- )
79
- timely_response = (
80
- data[data["Timely response?"] == "Yes"]
81
- .groupby("Year")
82
- .agg({"Complaint ID": "count"})
83
- .rename(columns={"Complaint ID": "Timely response"})
84
- .reset_index()
85
- )
86
- closed_without_cost = (
87
- data[data["Company response - Closed"] != "Closed with monetary relief"]
88
- .groupby("Year")
89
- .agg({"Complaint ID": "count"})
90
- .rename(columns={"Complaint ID": "Closed w/o cost"})
91
- .reset_index()
92
- )
93
- consumer_disputed = (
94
- data[data["Consumer disputed?"] == "Yes"]
95
- .groupby("Year")
96
- .agg({"Complaint ID": "count"})
97
- .rename(columns={"Complaint ID": "Consumer disputed"})
98
- .reset_index()
99
- )
100
-
101
- # Merge all data frames into one
102
- dfs_to_merge = [total_complaints, closed_complaints, timely_response, closed_without_cost, consumer_disputed]
103
- df_kpi = reduce(lambda left, right: pd.merge(left, right, on="Year", how="outer"), dfs_to_merge)
104
-
105
- # Calculate percentages
106
- df_kpi.fillna(0, inplace=True)
107
- df_kpi["Closed Complaints"] = df_kpi["Closed Complaints"] / df_kpi["Total Complaints"] * 100
108
- df_kpi["Open Complaints"] = 100 - df_kpi["Closed Complaints"]
109
- df_kpi["Timely response"] = df_kpi["Timely response"] / df_kpi["Total Complaints"] * 100
110
- df_kpi["Closed w/o cost"] = df_kpi["Closed w/o cost"] / df_kpi["Total Complaints"] * 100
111
- df_kpi["Consumer disputed"] = df_kpi["Consumer disputed"] / df_kpi["Total Complaints"] * 100
112
-
113
- # Pivot the dataframe and flatten
114
- df_kpi["index"] = 0
115
- df_kpi = df_kpi.pivot(
116
- index="index",
117
- columns="Year",
118
- values=[
119
- "Total Complaints",
120
- "Closed Complaints",
121
- "Open Complaints",
122
- "Timely response",
123
- "Closed w/o cost",
124
- "Consumer disputed",
125
- ],
126
- )
127
- df_kpi.columns = [f"{kpi}_{year}" for kpi, year in df_kpi.columns]
128
- return df_kpi
 
1
  """Contains helper functions and variables."""
2
 
 
 
3
  import numpy as np
4
  import pandas as pd
5
 
 
38
  data["Company response - detailed"] = data["Company response - detailed"].replace("Closed", "Closed without relief")
39
  data["State"] = data["State"].replace("UNITED STATES MINOR OUTLYING ISLANDS", "UM")
40
  data["State"] = fill_na_with_random(data, "State")
 
41
 
42
  # Convert to correct data type
43
  data["Date Received"] = pd.to_datetime(data["Date Received"], format="%m/%d/%y").dt.strftime("%Y-%m-%d")
44
+ data["Date Submitted"] = pd.to_datetime(data["Date Submitted"], format="%m/%d/%y").dt.strftime("%Y-%m-%d")
45
 
46
  # Create additional columns
47
+ data["Year-Month Received"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%Y-%m")
 
48
  data["Region"] = data["State"].map(REGION_MAPPING)
49
  data["Company response"] = np.where(
50
  data["Company response - detailed"].str.contains("Closed"), "Closed", data["Company response - detailed"]
 
52
  data["Company response - Closed"] = np.where(
53
  data["Company response - detailed"].str.contains("Closed"), data["Company response - detailed"], "Not closed"
54
  )
55
+ return data