li-nguyen commited on
Commit
02b2466
Β·
1 Parent(s): cc30cb8

Bring over iterations

Browse files
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: KPI Demo
3
  emoji: πŸ“Š
4
  colorFrom: blue
5
  colorTo: blue
@@ -11,26 +11,41 @@ short_description: Example of a Key Performance Indicator (KPI) dashboard
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
 
1
  ---
2
+ title: KPI Dashboard
3
  emoji: πŸ“Š
4
  colorFrom: blue
5
  colorTo: blue
 
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/.
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -2,113 +2,123 @@
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 goes 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
  )
@@ -123,26 +133,22 @@ page_exec = vm.Page(
123
  layout=vm.Layout(
124
  grid=[
125
  [0, 0],
 
 
126
  [1, 2],
127
  [1, 2],
128
  [1, 3],
129
  [1, 3],
 
130
  ],
131
  ),
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,23 +159,15 @@ page_exec = vm.Page(
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,26 +179,26 @@ page_region = vm.Page(
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,7 +230,7 @@ page_table = vm.Page(
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=[
@@ -247,5 +245,5 @@ dashboard = vm.Dashboard(
247
  app = Vizro().build(dashboard)
248
  server = app.dash.server
249
 
250
- if __name__ == "__main__":
251
- app.run()
 
2
 
3
  import pandas as pd
4
  import vizro.models as vm
5
+ from utils._charts import COLUMN_DEFS, FlexContainer, area, bar, choropleth, pie
6
+ from utils._helper import clean_data_and_add_columns, create_data_for_kpi_cards
7
  from vizro import Vizro
8
  from vizro.actions import filter_interaction
9
+ from vizro.figures import kpi_card_reference
10
  from vizro.tables import dash_ag_grid
11
 
12
  # DATA --------------------------------------------------------------------------------------------
13
  df_complaints = pd.read_csv("https://query.data.world/s/glbdstahsuw3hjgunz3zssggk7dsfu?dws=00000")
14
  df_complaints = clean_data_and_add_columns(df_complaints)
15
+ df_kpi_cards = create_data_for_kpi_cards(df_complaints)
16
+ vm.Page.add_type("components", FlexContainer)
17
+
18
 
19
  # SUB-SECTIONS ------------------------------------------------------------------------------------
20
+ kpi_banner = FlexContainer(
 
 
21
  components=[
22
+ vm.Figure(
23
+ id="kpi-reverse-coloring",
24
+ figure=kpi_card_reference(
25
+ df_kpi_cards,
26
+ value_column="Total Complaints_2019",
27
+ reference_column="Total Complaints_2018",
28
+ title="Total Complaints",
29
+ value_format="{value:.0f}",
30
+ reference_format="{delta_relative:+.1%} vs. 2018 ({reference:.0f})",
31
+ ),
 
 
 
 
32
  ),
33
+ vm.Figure(
34
+ figure=kpi_card_reference(
35
+ df_kpi_cards,
36
+ value_column="Closed Complaints_2019",
37
+ reference_column="Closed Complaints_2018",
38
+ title="Closed Complaints",
39
+ value_format="{value:.1f}%",
40
+ reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
41
+ )
42
  ),
43
+ vm.Figure(
44
+ figure=kpi_card_reference(
45
+ df_kpi_cards,
46
+ value_column="Timely response_2019",
47
+ reference_column="Timely response_2018",
48
+ title="Timely Response",
49
+ value_format="{value:.1f}%",
50
+ reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
51
+ )
52
  ),
53
+ vm.Figure(
54
+ figure=kpi_card_reference(
55
+ df_kpi_cards,
56
+ value_column="Closed w/o cost_2019",
57
+ reference_column="Closed w/o cost_2018",
58
+ title="Closed w/o cost",
59
+ value_format="{value:.1f}%",
60
+ reference_format="{delta:.1f}pp vs. 2018 ({reference:.1f}%)",
61
+ )
62
  ),
63
+ vm.Figure(
64
+ figure=kpi_card_reference(
65
+ df_kpi_cards,
66
+ value_column="Consumer disputed_2019",
67
+ reference_column="Consumer disputed_2018",
68
+ title="Consumer disputed",
69
+ value_format="{value:.1f}%",
70
+ reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
71
+ )
72
  ),
73
  ],
74
+ classname="kpi-banner",
75
  )
76
 
77
  bar_charts_tabbed = vm.Tabs(
78
  tabs=[
79
  vm.Container(
80
+ title="By Product",
81
  components=[
82
  vm.Graph(
83
  figure=bar(
84
  data_frame=df_complaints,
85
+ y="Product",
86
  x="Complaint ID",
87
  ),
88
  )
89
  ],
90
  ),
91
  vm.Container(
92
+ title="By Channel",
93
  components=[
94
  vm.Graph(
95
  figure=bar(
96
  data_frame=df_complaints,
97
+ y="Channel",
98
  x="Complaint ID",
99
  ),
100
  )
101
  ],
102
  ),
103
  vm.Container(
104
+ title="By Region",
105
  components=[
106
  vm.Graph(
107
  figure=bar(
108
  data_frame=df_complaints,
109
+ y="Region",
110
  x="Complaint ID",
111
  ),
112
  )
113
  ],
114
  ),
115
  vm.Container(
116
+ title="By Issue",
117
  components=[
118
  vm.Graph(
119
  figure=bar(
120
  data_frame=df_complaints,
121
+ y="Issue",
122
  x="Complaint ID",
123
  ),
124
  )
 
133
  layout=vm.Layout(
134
  grid=[
135
  [0, 0],
136
+ [0, 0],
137
+ [1, 2],
138
  [1, 2],
139
  [1, 2],
140
  [1, 3],
141
  [1, 3],
142
+ [1, 3],
143
  ],
144
  ),
145
  components=[
146
  kpi_banner,
147
  bar_charts_tabbed,
148
+ vm.Graph(figure=area(data_frame=df_complaints, y="Complaint ID", x="Month")),
149
  vm.Graph(
150
  figure=pie(
151
  data_frame=df_complaints[df_complaints["Company response - Closed"] != "Not closed"],
 
 
 
 
 
 
 
152
  values="Complaint ID",
153
  names="Company response - Closed",
154
  title="Closed company responses",
 
159
 
160
  page_region = vm.Page(
161
  title="Regional View",
162
+ layout=vm.Layout(grid=[[0, 1]]),
163
  components=[
 
 
 
 
 
 
 
 
 
164
  vm.Graph(
165
  figure=choropleth(
166
  data_frame=df_complaints,
167
  locations="State",
168
  color="Complaint ID",
169
+ title="Complaints by State <br><sup> ‡ Click on a state to filter the "
170
+ "charts on the right. Refresh the page to deselect.</sup>",
171
  custom_data=["State"],
172
  ),
173
  actions=[
 
179
  vm.Tabs(
180
  tabs=[
181
  vm.Container(
182
+ title="By Product",
183
  components=[
184
  vm.Graph(
185
+ id="regional-product",
186
  figure=bar(
187
  data_frame=df_complaints,
188
+ y="Product",
189
  x="Complaint ID",
190
  ),
191
  )
192
  ],
193
  ),
194
  vm.Container(
195
+ title="By Issue",
196
  components=[
197
  vm.Graph(
198
+ id="regional-issue",
199
  figure=bar(
200
  data_frame=df_complaints,
201
+ y="Issue",
202
  x="Complaint ID",
203
  ),
204
  )
 
230
 
231
  dashboard = vm.Dashboard(
232
  pages=[page_exec, page_region, page_table],
233
+ title="Cumulus Financial Corp. - Fiscal Year 2019",
234
  navigation=vm.Navigation(
235
  nav_selector=vm.NavBar(
236
  items=[
 
245
  app = Vizro().build(dashboard)
246
  server = app.dash.server
247
 
248
+ if __name__ == "__main__":
249
+ app.run()
assets/css/custom.css CHANGED
@@ -2,78 +2,39 @@
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
  }
 
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
  }
assets/images/kpi_dashboard.gif DELETED

Git LFS Details

  • SHA256: 142385ec4872ebb32fe7405c8df2c5e93915fde660c053c374e2b27bd0c45ee5
  • Pointer size: 133 Bytes
  • Size of remote file: 15.6 MB
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 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,35 +11,23 @@ from vizro.models.types import capture
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
 
 
 
 
43
  # CUSTOM CHARTS ----------------------------------------------------------------
44
  @capture("graph")
45
  def bar(
@@ -49,8 +37,11 @@ def bar(
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,21 +51,38 @@ def bar(
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,25 +92,29 @@ def pie(
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,8 +126,11 @@ def choropleth(
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,8 +154,7 @@ def choropleth(
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,4 +204,4 @@ COLUMN_DEFS = [
190
  "flex": 6,
191
  },
192
  {"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3},
193
- ]
 
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
 
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
 
28
+ vm.Container.add_type("components", FlexContainer)
29
+
30
+
31
  # CUSTOM CHARTS ----------------------------------------------------------------
32
  @capture("graph")
33
  def bar(
 
37
  top_n: int = 15,
38
  custom_data: Optional[List[str]] = None,
39
  ):
40
+ """Custom bar chart implementation.
41
 
42
+ Based on [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar).
43
+ """
44
+ df_agg = data_frame.groupby(y).agg({x: "count"}).sort_values(by=x, ascending=False).reset_index()
45
  fig = px.bar(
46
  data_frame=df_agg.head(top_n),
47
  x=x,
 
51
  color_discrete_sequence=["#1A85FF"],
52
  custom_data=custom_data,
53
  )
54
+ fig.update_layout(xaxis_title="# of Complaints", yaxis={"title": "", "autorange": "reversed"})
55
  return fig
56
 
57
 
58
  @capture("graph")
59
+ def area(x: str, y: str, data_frame: pd.DataFrame):
60
+ """Custom chart to create unstacked area chart.
61
+
62
+ Based on [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html).
63
+
64
+ """
65
+ df_agg = data_frame.groupby(["Year", "Month"]).agg({y: "count"}).reset_index()
66
+ df_agg_2019 = df_agg[df_agg["Year"] == "2018"]
67
+ df_agg_2020 = df_agg[df_agg["Year"] == "2019"]
68
+
69
+ fig = go.Figure()
70
+ fig.add_trace(
71
+ go.Scatter(x=df_agg_2020[x], y=df_agg_2020[y], fill="tozeroy", name="2019", marker={"color": "#1a85ff"})
72
+ )
73
+ fig.add_trace(go.Scatter(x=df_agg_2019[x], y=df_agg_2019[y], fill="tonexty", name="2018", marker={"color": "grey"}))
74
+ fig.update_layout(
75
  title="Complaints over time",
76
+ xaxis_title="Date Received",
77
+ yaxis_title="# of Complaints",
78
+ title_pad_t=4,
79
+ xaxis={
80
+ "showgrid": False,
81
+ "tickmode": "array",
82
+ "tickvals": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
83
+ "ticktext": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
84
+ },
85
  )
 
86
  return fig
87
 
88
 
 
92
  values: str,
93
  data_frame: pd.DataFrame = None,
94
  title: Optional[str] = None,
 
95
  ):
96
+ """Custom pie chart implementation.
 
 
 
 
97
 
98
+ Based on [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie).
99
+ """
100
+ df_agg = data_frame.groupby(names).agg({values: "count"}).reset_index()
101
  fig = px.pie(
102
+ data_frame=df_agg,
103
  names=names,
104
  values=values,
105
+ color=names,
106
+ color_discrete_map={
107
+ "Closed with explanation": "#1a85ff",
108
+ "Closed with monetary relief": "#d41159",
109
+ "Closed with non-monetary relief": "#adbedc",
110
+ "Closed without relief": "#7ea1ee",
111
+ "Closed with relief": "#df658c",
112
+ "Closed": "#1a85ff",
113
+ },
114
  title=title,
115
  hole=0.4,
116
  )
117
+ fig.update_layout(legend_x=1, legend_y=1, title_pad_t=2, margin={"l": 0, "r": 0, "t": 60, "b": 0})
 
 
118
  return fig
119
 
120
 
 
126
  title: Optional[str] = None,
127
  custom_data: Optional[List[str]] = None,
128
  ):
129
+ """Custom choropleth implementation.
130
 
131
+ Based on [px.choropleth](https://plotly.com/python-api-reference/generated/plotly.express.choropleth).
132
+ """
133
+ df_agg = data_frame.groupby(locations).agg({color: "count"}).reset_index()
134
  fig = px.choropleth(
135
  data_frame=df_agg,
136
  locations=locations,
 
154
  title=title,
155
  custom_data=custom_data,
156
  )
157
+ fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "bottom"}, "orientation": "h", "x": 0.5, "y": 0})
 
158
  return fig
159
 
160
 
 
204
  "flex": 6,
205
  },
206
  {"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3},
207
+ ]
utils/_helper.py CHANGED
@@ -1,5 +1,7 @@
1
  """Contains helper functions and variables."""
2
 
 
 
3
  import numpy as np
4
  import pandas as pd
5
 
@@ -38,13 +40,14 @@ def clean_data_and_add_columns(data: pd.DataFrame):
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,4 +55,74 @@ def clean_data_and_add_columns(data: pd.DataFrame):
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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
  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