Spaces:
Running
Running
Bring over iterations
Browse files- README.md +33 -18
- app.py +78 -80
- assets/css/custom.css +20 -59
- assets/images/kpi_dashboard.gif +0 -3
- utils/__init__.py +1 -1
- utils/_charts.py +64 -50
- utils/_helper.py +76 -3
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title: KPI
|
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
|
15 |
-
It uses fictional budget data to demonstrate the capabilities of Vizro using real world applications.
|
16 |
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
provides high-quality fake data for creating realistic dashboard examples for real-world applications.
|
19 |
|
20 |
-
Note
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
-
##
|
25 |
|
26 |
-
|
27 |
-
|
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,
|
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 |
-
|
|
|
|
|
15 |
|
16 |
# SUB-SECTIONS ------------------------------------------------------------------------------------
|
17 |
-
kpi_banner =
|
18 |
-
id="kpi-banner",
|
19 |
-
title="",
|
20 |
components=[
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
value="99.6%",
|
32 |
-
icon="arrow_circle_up",
|
33 |
-
sign="delta-pos",
|
34 |
-
ref_value="+0.2% vs. LY",
|
35 |
),
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
42 |
),
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
49 |
),
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
56 |
),
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
|
|
|
|
|
|
63 |
),
|
64 |
],
|
|
|
65 |
)
|
66 |
|
67 |
bar_charts_tabbed = vm.Tabs(
|
68 |
tabs=[
|
69 |
vm.Container(
|
70 |
-
title="By
|
71 |
components=[
|
72 |
vm.Graph(
|
73 |
figure=bar(
|
74 |
data_frame=df_complaints,
|
75 |
-
y="
|
76 |
x="Complaint ID",
|
77 |
),
|
78 |
)
|
79 |
],
|
80 |
),
|
81 |
vm.Container(
|
82 |
-
title="By
|
83 |
components=[
|
84 |
vm.Graph(
|
85 |
figure=bar(
|
86 |
data_frame=df_complaints,
|
87 |
-
y="
|
88 |
x="Complaint ID",
|
89 |
),
|
90 |
)
|
91 |
],
|
92 |
),
|
93 |
vm.Container(
|
94 |
-
title="By
|
95 |
components=[
|
96 |
vm.Graph(
|
97 |
figure=bar(
|
98 |
data_frame=df_complaints,
|
99 |
-
y="
|
100 |
x="Complaint ID",
|
101 |
),
|
102 |
)
|
103 |
],
|
104 |
),
|
105 |
vm.Container(
|
106 |
-
title="By
|
107 |
components=[
|
108 |
vm.Graph(
|
109 |
figure=bar(
|
110 |
data_frame=df_complaints,
|
111 |
-
y="
|
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=
|
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,
|
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
|
185 |
components=[
|
186 |
vm.Graph(
|
187 |
-
id="regional-
|
188 |
figure=bar(
|
189 |
data_frame=df_complaints,
|
190 |
-
y="
|
191 |
x="Complaint ID",
|
192 |
),
|
193 |
)
|
194 |
],
|
195 |
),
|
196 |
vm.Container(
|
197 |
-
title="By
|
198 |
components=[
|
199 |
vm.Graph(
|
200 |
-
id="regional-
|
201 |
figure=bar(
|
202 |
data_frame=df_complaints,
|
203 |
-
y="
|
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
|
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 |
-
|
|
|
7 |
}
|
8 |
|
9 |
-
.kpi-
|
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 |
-
|
20 |
-
|
|
|
21 |
}
|
22 |
|
23 |
-
.kpi-
|
24 |
-
|
25 |
}
|
26 |
|
27 |
-
.kpi-
|
28 |
-
|
29 |
}
|
30 |
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
}
|
35 |
|
36 |
-
.kpi-card-
|
37 |
-
|
38 |
-
flex-direction: row;
|
39 |
-
gap: 8px;
|
40 |
-
margin: 0;
|
41 |
}
|
42 |
|
43 |
-
|
44 |
-
|
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-
|
77 |
-
border:
|
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
|
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 |
-
|
15 |
-
|
16 |
-
|
17 |
-
""
|
18 |
-
|
19 |
-
|
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 |
-
|
28 |
-
|
29 |
-
|
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 |
-
|
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=
|
64 |
return fig
|
65 |
|
66 |
|
67 |
@capture("graph")
|
68 |
-
def
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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=
|
97 |
names=names,
|
98 |
values=values,
|
99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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["
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|