antonymilne commited on
Commit
4cfea5b
·
verified ·
1 Parent(s): d00c79f

Tidy and a couple of new features (#2)

Browse files

- Refactor overview page to simplify navigation and reduce code repetition (e6217b4cc55dea4c4f9dba7ed7abad845d5df17c)
- Further refactor KPI cards and simplify page structure (5d3ed2fba6752b0a5f2db4b41368721fe1786008)
- Simplify actions arguments and remove unnecessary trailing commas (7795980e5fe8aacbd1eb39c47c459a6f991f7d84)
- Remove unnecessary trailing commas from last items in structures (0b9114b421e1bc959742cf9ba64ef93c1b7f6353)
- Move variant argument to last position in Container (f3c4f1b9e84368fd4d94903569cd4c6fbec39190)
- Refactor navigation using list comprehension (ed14a62117822c93730d4a59488038abdf79f58a)
- Fix argument ordering in Container definitions (c62cec4a2eab3d93eddc7501126e3562e9a823f8)
- Convert pages to dictionary with nav icons as values (1e4d086ba6dba2d35ad93b26388e88c432ca36b1)
- Swap dictionary keys and values for pages (3399bddfa999cf14613fdd4ad28d3dff9718ad51)
- Update CSS selectors to match renamed IDs (4afa104559cd97f52ca16699ff3fbebce02a0ac5)
- Refactor and clean up data processing and app code (32b31c949621adcd3c8991903b9e3e73597389b6)
- Add docstrings and rename data.py to data_processing.py (756cacef8e2672ef000fb9ec29250b6842031359)
- Separate AG Grid tables from Plotly charts (d267326fe2e70010c578af3cb0ef380b9f34295c)
- Refactor KPI cards and chart functions with shared constants (22e2ea0e7075da2e87d37f4a3b0a5a7275e4dc8b)
- Simplify overview_by_order_status function (0511d441b291b9d9decd7dcc1506deb9f099f29a)
- Refactor overview charts with shared helper functions (a4e7aa2147784fb942480374062e9bdb2891e620)
- Reorder chart functions by usage order in app.py (82a8080b980ebedf6f75ce2b2f15403967e4cd8a)
- Refactor and simplify Pareto chart with improved data processing (8ba00b62948109e54726dcdb8b442977cc42175e)
- Clean up imports and CSS formatting (a72697dfc02eeb70b9acb6cd288e209effec14a4)

__pycache__/app.cpython-312.pyc ADDED
Binary file (11.4 kB). View file
 
__pycache__/charts.cpython-312.pyc ADDED
Binary file (17.8 kB). View file
 
__pycache__/data_processing.cpython-312.pyc ADDED
Binary file (3.44 kB). View file
 
app.py CHANGED
@@ -1,505 +1,298 @@
1
  """Dev app to try things out."""
2
 
3
- import json
4
- import base64
5
-
6
- import vizro.plotly.express as px
7
- from vizro import Vizro
8
- import vizro.models as vm
9
  import vizro.actions as va
10
- from vizro.figures import kpi_card, kpi_card_reference
11
- from vizro.models.types import capture
12
- from vizro.tables import dash_ag_grid
13
-
14
- from data import superstore_df, create_superstore_product, pareto_customers_table, create_kpi_data
15
  from charts import (
16
  bar_chart_by_category,
17
- create_map_bubble_new,
18
- create_line_chart_per_month,
19
- create_bar_current_vs_previous_segment,
20
- create_bar_current_vs_previous_category,
 
21
  pareto_customers_chart,
 
 
22
  scatter_with_quadrants,
23
- pie_chart_by_order_status,
24
- bar_chart_top_n,
25
- custom_orders_aggrid,
26
- create_lollipop_chart_by_region,
27
  )
28
- from charts import COLUMN_DEFS_PRODUCT, COLUMN_DEFS_CUSTOMERS
29
-
30
- df = px.data.iris()
31
-
32
- vm.Page.add_type("controls", vm.Button)
33
- vm.Container.add_type("controls", vm.Button)
34
-
35
- superstore_product_df = create_superstore_product(superstore_df)
36
- aggrid_df = pareto_customers_table(superstore_df)
37
- state_list = superstore_df["State_Code"].unique().tolist()
38
- categories = superstore_df["Category"].unique().tolist()
39
- subcategories = superstore_df["Sub-Category"].unique().tolist()
40
- subcategories.append("NONE")
41
- customer_name = superstore_df["Customer Name"].unique().tolist()
42
- customer_name.append("NONE")
43
- product_name = superstore_df["Product Name"].unique().tolist()
44
- product_name.append("NONE")
45
-
46
- sales_kpi_df = create_kpi_data(superstore_df, value_col="Sales")
47
- profit_kpi_df = create_kpi_data(superstore_df, value_col="Profit")
48
- order_kpi_df = create_kpi_data(superstore_df, value_col="Order ID")
49
- customer_kpi_df = create_kpi_data(superstore_df, value_col="Customer ID")
50
 
 
 
 
 
 
51
 
52
- def _encode_to_base64(value):
53
- json_bytes = json.dumps(value, separators=(",", ":")).encode("utf-8")
54
- b64_bytes = base64.urlsafe_b64encode(json_bytes)
55
- return f"b64_{b64_bytes.decode('utf-8').rstrip('=')}"
56
 
 
 
57
 
58
- @capture("action")
59
- def nav_region(parameter_value):
60
- url_query_params = f"?pg2-parameter-2={_encode_to_base64(parameter_value)}"
61
- return "/regions", url_query_params
62
 
 
 
 
 
 
 
 
63
 
64
- @capture("action")
65
- def nav_product():
66
- return "/products"
 
 
 
 
 
 
 
 
 
67
 
68
 
69
- @capture("action")
70
- def nav_customer():
71
- return "/customers"
 
72
 
73
 
74
- @capture("action")
75
- def nav_orders():
76
- return "/orders"
77
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- page_1 = vm.Page(
80
  title="Overview",
 
 
81
  components=[
82
- vm.Container(
83
- id="pg1-container-1",
84
- title="💡 Click on a KPI card to update the charts below.",
85
- components=[
86
- vm.Figure(
87
- figure=kpi_card_reference(
88
- data_frame=sales_kpi_df,
89
- value_column="total_2017",
90
- reference_column="total_2016",
91
- reference_format="{delta_relative:+.1%} vs. last year (${reference:,.0f})",
92
- title="Sales",
93
- value_format="${value:,.0f}",
94
- icon="bar_chart",
95
- ),
96
- actions=va.set_control(control="pg1_parameter_1", value="Sales"),
97
- ),
98
- vm.Figure(
99
- figure=kpi_card_reference(
100
- data_frame=profit_kpi_df,
101
- value_column="total_2017",
102
- reference_column="total_2016",
103
- reference_format="{delta_relative:+.1%} vs. last year (${reference:,.0f})",
104
- title="Profit",
105
- value_format="${value:,.0f}",
106
- icon="money_bag",
107
- ),
108
- actions=va.set_control(control="pg1_parameter_1", value="Profit"),
109
- ),
110
- vm.Figure(
111
- figure=kpi_card_reference(
112
- data_frame=order_kpi_df,
113
- value_column="total_2017",
114
- reference_column="total_2016",
115
- reference_format="{delta_relative:+.1%} vs. last year ({reference:,.0f})",
116
- title="Orders",
117
- value_format="{value:,.0f}",
118
- icon="orders",
119
- # agg_func="nunique",
120
- ),
121
- actions=va.set_control(control="pg1_parameter_1", value="Order ID"),
122
- ),
123
- vm.Figure(
124
- figure=kpi_card_reference(
125
- data_frame=customer_kpi_df,
126
- value_column="total_2017",
127
- reference_column="total_2016",
128
- title="Customers",
129
- reference_format="{delta_relative:+.1%} vs. last year ({reference:,.0f})",
130
- value_format="{value:,.0f}",
131
- icon="group",
132
- # agg_func="nunique",
133
- ),
134
- actions=va.set_control(control="pg1_parameter_1", value="Customer ID"),
135
- ),
136
- ],
137
- layout=vm.Grid(grid=[[0, 1, 2, 3]]),
138
- ),
139
- vm.Container(
140
- title="",
141
- components=[
142
- vm.Graph(
143
- id="line_chart_by_month",
144
- figure=create_line_chart_per_month(
145
- superstore_df,
146
- value_col="Sales",
147
- ),
148
- ),
149
- ],
150
- variant="filled",
151
  ),
152
- vm.Container(
153
- title="",
154
- components=[
155
- vm.Graph(id="order_status_chart", figure=pie_chart_by_order_status(superstore_df, value_col="Sales")),
156
- vm.Button(
157
- id="orders-nav-btn",
158
- text="View Deep Dive",
159
- icon="jump_to_element",
160
- variant="outlined",
161
- actions=vm.Action(function=nav_orders(), outputs=["vizro_url.pathname"]),
162
- ),
163
- ],
164
- layout=vm.Grid(grid=[[0], [0], [0], [0], [0], [1]], row_gap="8px"),
165
- variant="filled",
166
  ),
167
- vm.Container(
168
- components=[
169
- vm.Graph(
170
- id="region_bar_chart", figure=create_lollipop_chart_by_region(superstore_df, value_col="Sales")
171
- ),
172
- vm.Button(
173
- id="region-nav-btn",
174
- text="View Deep Dive",
175
- icon="jump_to_element",
176
- variant="outlined",
177
- actions=vm.Action(
178
- function=nav_region("pg1_parameter_1"), outputs=["vizro_url.pathname", "vizro_url.search"]
179
- ),
180
- ),
181
- ],
182
- layout=vm.Grid(grid=[[0], [0], [0], [0], [0], [1]], row_gap="8px"),
183
- variant="filled",
184
  ),
185
- vm.Container(
186
- components=[
187
- vm.Graph(
188
- id="bar_chart_by_segment",
189
- figure=create_bar_current_vs_previous_segment(superstore_df, value_col="Sales"),
190
- ),
191
- vm.Button(
192
- id="segment-nav-btn",
193
- text="View Deep Dive",
194
- icon="jump_to_element",
195
- variant="outlined",
196
- actions=vm.Action(function=nav_customer(), outputs=["vizro_url.pathname"]),
197
- ),
198
- ],
199
- variant="filled",
200
- layout=vm.Grid(grid=[[0], [0], [0], [0], [0], [1]], row_gap="8px"),
201
  ),
202
- vm.Container(
203
- title="",
204
- layout=vm.Grid(grid=[[0], [0], [0], [0], [0], [1]], row_gap="8px"),
205
- components=[
206
- vm.Graph(
207
- id="category_bar_chart",
208
- figure=create_bar_current_vs_previous_category(superstore_df, value_col="Sales"),
209
- ),
210
- vm.Button(
211
- id="customer-nav-btn",
212
- text="View Deep Dive",
213
- icon="jump_to_element",
214
- variant="outlined",
215
- actions=vm.Action(function=nav_product(), outputs=["vizro_url.pathname"]),
216
- ),
217
- ],
218
- variant="filled",
219
  ),
220
  ],
221
  controls=[
222
  vm.Parameter(
223
- id="pg1_parameter_1",
224
- selector=vm.RadioItems(
225
- options=[
226
- {"value": "Sales", "label": "Sales"},
227
- {"value": "Profit", "label": "Profit"},
228
- {"value": "Order ID", "label": "Orders"},
229
- {"value": "Customer ID", "label": "Customers"},
230
- ],
231
- title="Metric",
232
- ),
233
  targets=[
234
- "region_bar_chart.value_col",
235
- "category_bar_chart.value_col",
236
- "order_status_chart.value_col",
237
- "line_chart_by_month.value_col",
238
- "bar_chart_by_segment.value_col",
239
  ],
240
- show_in_url=True,
241
  visible=False,
242
- ),
243
  ],
244
- layout=vm.Grid(
245
- grid=[
246
- [0, 0, 0],
247
- [0, 0, 0],
248
- [0, 0, 0],
249
- [1, 1, 2],
250
- [1, 1, 2],
251
- [1, 1, 2],
252
- [1, 1, 2],
253
- [1, 1, 2],
254
- [3, 4, 5],
255
- [3, 4, 5],
256
- [3, 4, 5],
257
- [3, 4, 5],
258
- [3, 4, 5],
259
- ],
260
- ),
261
  )
262
 
263
- page_2 = vm.Page(
264
- title="Regions",
 
 
265
  components=[
266
- vm.Container(
267
- controls=[
268
- vm.Filter(
269
- id="pg2-filter-1",
270
- column="State_Code",
271
- selector=vm.Dropdown(),
272
- visible=False,
273
- ),
274
- vm.Filter(
275
- column="Segment",
276
- selector=vm.Checklist(title="Choose segment"),
277
- ),
278
- vm.Filter(
279
- column="Category",
280
- selector=vm.Checklist(title="Choose category"),
281
- ),
282
- vm.Parameter(
283
- id="pg2-parameter-2",
284
- selector=vm.RadioItems(
285
- options=["Sales", "Profit"],
286
- title="Choose metric",
287
- ),
288
- targets=[
289
- "region_map_chart.value_col",
290
- "pg2-chart-1.x",
291
- ],
292
- ),
293
- ],
294
- layout=vm.Grid(grid=[[0, 1]]),
295
- components=[
296
- vm.Container(
297
- variant="filled",
298
- title="",
299
- components=[
300
- vm.Graph(
301
- id="region_map_chart",
302
- header="💡 Click on a state to filter charts on the right.",
303
- figure=create_map_bubble_new(superstore_df, value_col="Sales", custom_data=["State_Code"]),
304
- actions=[
305
- va.set_control(control="pg2-filter-1", value="State_Code"),
306
- ],
307
- ),
308
- ],
309
- ),
310
- vm.Container(
311
- controls=[
312
- vm.Parameter(
313
- targets=["pg2-chart-1.top_n"],
314
- selector=vm.Slider(min=5, max=30, step=5, value=20, title="Choose top N"),
315
- ),
316
- vm.Parameter(
317
- selector=vm.RadioItems(
318
- options=["City", "Customer Name", "Sub-Category"],
319
- title="Choose y-axis",
320
- ),
321
- targets=["pg2-chart-1.y"],
322
- ),
323
- ],
324
- components=[
325
- vm.Graph(
326
- id="pg2-chart-1",
327
- figure=bar_chart_top_n(superstore_df, x="Sales", y="City"),
328
- ),
329
- ],
330
- variant="filled",
331
- ),
332
- ],
333
  ),
334
  ],
335
  )
336
 
337
- page_3 = vm.Page(
338
- title="Customers",
339
  components=[
340
  vm.Container(
341
  layout=vm.Grid(grid=[[0, 1]]),
 
342
  controls=[
343
- vm.Filter(column="Region", selector=vm.Checklist(title="Choose region")),
344
  vm.Filter(column="Segment", selector=vm.Checklist(title="Choose segment")),
345
  vm.Filter(column="Category", selector=vm.Checklist(title="Choose category")),
346
- ],
347
- components=[
348
- vm.AgGrid(
349
- id="table-2",
350
- header="💡 Click on a row to highlight the customer.",
351
- figure=dash_ag_grid(
352
- aggrid_df,
353
- columnDefs=COLUMN_DEFS_CUSTOMERS,
354
- ),
355
- actions=va.set_control(control="pg3_parameter_1", value="Customer Name"),
356
  ),
357
- vm.Graph(id="pg3_pareto_chart", figure=pareto_customers_chart(superstore_df, highlight_customer=None)),
358
  ],
359
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  ],
361
  controls=[
362
  vm.Parameter(
363
- id="pg3_parameter_1",
364
- targets=["pg3_pareto_chart.highlight_customer"],
365
- selector=vm.Dropdown(options=customer_name, value="NONE", multi=False),
366
  visible=False,
367
  ),
368
  ],
369
  )
370
 
 
 
371
 
372
- page_4 = vm.Page(
373
  title="Products",
374
  components=[
375
  vm.Container(
376
- title="",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  components=[
378
- vm.Container(
379
- title="",
380
- components=[
381
- vm.Graph(
382
- id="pg4-chart-1",
383
- figure=bar_chart_by_category(superstore_df, custom_data=["Category"]),
384
- actions=[
385
- va.set_control(control="pg4-filter-1", value="Category"),
386
- ],
387
- ),
388
- ],
389
- variant="filled",
390
- ),
391
- vm.Container(
392
- title="",
393
- components=[
394
- vm.AgGrid(
395
- id="table",
396
- header="💡 Click on a row to highlight the data point in the matrix on the right.",
397
- figure=dash_ag_grid(superstore_product_df, columnDefs=COLUMN_DEFS_PRODUCT),
398
- actions=va.set_control(control="pg4_parameter_1", value="Sub-Category"),
399
- )
400
- ],
401
- variant="filled",
402
  ),
403
- vm.Container(
404
- title="",
405
- components=[
406
- vm.Graph(
407
- id="pg4-chart-3",
408
- figure=scatter_with_quadrants(
409
- data_frame=superstore_product_df,
410
- x="Sales",
411
- y="Profit",
412
- custom_data=["Sub-Category"],
413
- ),
414
- ),
415
- ],
416
- variant="filled",
417
  ),
418
  ],
419
- layout=vm.Grid(
420
- grid=[
421
- [0, 0, 2, 2],
422
- [1, 1, 2, 2],
423
- [1, 1, 2, 2],
424
- ]
425
- ),
426
- ),
427
- ],
428
- controls=[
429
- vm.Filter(
430
- id="pg4-filter-1",
431
- column="Category",
432
- targets=["pg4-chart-1"],
433
- selector=vm.Checklist(title="Product Category"),
434
- visible=False,
435
- ),
436
- # vm.Filter(
437
- # column="Category / Sub-Category",
438
- # targets=["pg4-chart-3"],
439
- # selector=vm.Dropdown(multi=False, value="Technology / Phones"),
440
- # ),
441
- vm.Parameter(
442
- id="pg4_parameter_1",
443
- targets=["pg4-chart-3.highlight_sub_category"],
444
- selector=vm.Dropdown(options=subcategories, value="NONE", multi=False),
445
- visible=False,
446
- ),
447
  ],
448
  )
449
 
450
- page_5 = vm.Page(
 
 
 
 
451
  title="Orders",
452
  layout=vm.Flex(),
453
  components=[
454
- vm.AgGrid(id="table_id", figure=custom_orders_aggrid(superstore_df)),
455
- vm.Button(
456
- text="Export data",
457
- icon="download",
458
- actions=[va.export_data(targets=["table_id"], file_format="xlsx")],
459
- ),
460
  ],
461
  )
462
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  navigation = vm.Navigation(
465
- pages=["Overview", "Regions", "Customers", "Products", "Orders"],
466
  nav_selector=vm.NavBar(
467
- items=[
468
- vm.NavLink(
469
- pages=["Overview"],
470
- label="Overview",
471
- icon="Home",
472
- ),
473
- vm.NavLink(
474
- pages=["Regions"],
475
- label="Regions",
476
- icon="Globe Asia",
477
- ),
478
- vm.NavLink(
479
- pages=["Products"],
480
- label="Products",
481
- icon="Barcode",
482
- ),
483
- vm.NavLink(
484
- pages=["Customers"],
485
- label="Customers",
486
- icon="Group",
487
- ),
488
- vm.NavLink(
489
- pages=["Orders"],
490
- icon="Shopping Cart",
491
- label="Orders",
492
- ),
493
- ]
494
  ),
495
  )
496
 
497
- dashboard = vm.Dashboard(
498
- title="Superstore dashboard",
499
- pages=[page_1, page_2, page_3, page_4, page_5],
500
- navigation=navigation,
501
- theme="vizro_light",
502
- )
503
 
504
  app = Vizro().build(dashboard)
505
 
 
1
  """Dev app to try things out."""
2
 
 
 
 
 
 
 
3
  import vizro.actions as va
4
+ import vizro.models as vm
 
 
 
 
5
  from charts import (
6
  bar_chart_by_category,
7
+ overview_by_customer_segment,
8
+ overview_by_month,
9
+ overview_by_order_status,
10
+ overview_by_product_category,
11
+ overview_by_region,
12
  pareto_customers_chart,
13
+ regions_map_chart,
14
+ regions_top_n_chart,
15
  scatter_with_quadrants,
 
 
 
 
16
  )
17
+ from data_processing import (
18
+ COLUMN_TO_AGGFUNC,
19
+ COLUMN_TO_METRIC,
20
+ LAST_YEAR,
21
+ THIS_YEAR,
22
+ make_superstore_df,
23
+ make_superstore_profit_df,
24
+ )
25
+ from tables import COLUMN_DEFS_PRODUCT, customers_ag_grid, orders_ag_grid
26
+ from vizro import Vizro
27
+ from vizro.figures import kpi_card_reference
28
+ from vizro.tables import dash_ag_grid
 
 
 
 
 
 
 
 
 
 
29
 
30
+ ###############################################################################
31
+ # Dataframes and utility functions
32
+ ###############################################################################
33
+ df = make_superstore_df()
34
+ profit_df = make_superstore_profit_df(df)
35
 
 
 
 
 
36
 
37
+ def create_filled_container(**kwargs):
38
+ return vm.Container(**kwargs, variant="filled")
39
 
 
 
 
 
40
 
41
+ ###############################################################################
42
+ # Overview page
43
+ ###############################################################################
44
+ def create_kpi_card_with_action(data_frame, column, icon, prefix=""):
45
+ """Create a KPI card figure that updates the metric control when clicked."""
46
+ # Pivot and aggregate to produce dataframe with column for each year for specified metric.
47
+ data_frame = data_frame.pivot_table(values=column, columns="Year", aggfunc=COLUMN_TO_AGGFUNC[column])
48
 
49
+ return vm.Figure(
50
+ figure=kpi_card_reference(
51
+ data_frame=data_frame,
52
+ value_column=THIS_YEAR, # This year
53
+ reference_column=LAST_YEAR, # Last year
54
+ value_format=f"{prefix}{{value:,.0f}}",
55
+ reference_format=f"{{delta_relative:+.1%}} vs. last year ({prefix}{{reference:,.0f}})",
56
+ title=COLUMN_TO_METRIC[column],
57
+ icon=icon,
58
+ ),
59
+ actions=va.set_control(control="metric", value=column),
60
+ )
61
 
62
 
63
+ def create_chart_container_with_nav(graph, nav_href):
64
+ """Create a container with a chart and optional navigation button."""
65
+ components = [graph, vm.Button(text="View Deep Dive", icon="Jump to Element", variant="outlined", href=nav_href)]
66
+ return create_filled_container(components=components, layout=vm.Grid(grid=[[0]] * 5 + [[1]], row_gap="8px"))
67
 
68
 
69
+ # TODO: chart title spacing, use container.title?
 
 
70
 
71
+ kpi_card_container = vm.Container(
72
+ id="kpi-cards",
73
+ title="💡 Click on a KPI card to update the charts below.",
74
+ layout=vm.Grid(grid=[[0, 1, 2, 3]]),
75
+ components=[
76
+ create_kpi_card_with_action(df, column="Sales", icon="Bar Chart", prefix="$"),
77
+ create_kpi_card_with_action(df, column="Profit", icon="Money Bag", prefix="$"),
78
+ create_kpi_card_with_action(df, column="Order ID", icon="Orders"),
79
+ create_kpi_card_with_action(df, column="Customer ID", icon="Group"),
80
+ ],
81
+ )
82
 
83
+ overview_page = vm.Page(
84
  title="Overview",
85
+ # Grid that consists of rows with height ratio 3:5:5.
86
+ layout=vm.Grid(grid=[[0, 0, 0]] * 3 + [[1, 1, 2]] * 5 + [[3, 4, 5]] * 5),
87
  components=[
88
+ kpi_card_container,
89
+ create_filled_container(
90
+ components=[vm.Graph(id="month_line_chart", figure=overview_by_month(df, column="Sales"))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  ),
92
+ create_chart_container_with_nav(
93
+ graph=vm.Graph(id="order_status_pie_chart", figure=overview_by_order_status(df, column="Sales")),
94
+ nav_href="/orders",
 
 
 
 
 
 
 
 
 
 
 
95
  ),
96
+ create_chart_container_with_nav(
97
+ graph=vm.Graph(id="region_bar_chart", figure=overview_by_region(df, column="Sales")),
98
+ nav_href="/regions",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  ),
100
+ create_chart_container_with_nav(
101
+ graph=vm.Graph(id="segment_bar_chart", figure=overview_by_customer_segment(df, column="Sales")),
102
+ nav_href="/customers",
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  ),
104
+ create_chart_container_with_nav(
105
+ graph=vm.Graph(id="category_bar_chart", figure=overview_by_product_category(df, column="Sales")),
106
+ nav_href="/products",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  ),
108
  ],
109
  controls=[
110
  vm.Parameter(
111
+ id="metric",
112
+ selector=vm.RadioItems(options=["Sales", "Profit", "Order ID", "Customer ID"]),
 
 
 
 
 
 
 
 
113
  targets=[
114
+ "region_bar_chart.column",
115
+ "category_bar_chart.column",
116
+ "order_status_pie_chart.column",
117
+ "month_line_chart.column",
118
+ "segment_bar_chart.column",
119
  ],
 
120
  visible=False,
121
+ )
122
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  )
124
 
125
+ ###############################################################################
126
+ # Regions page
127
+ ###############################################################################
128
+ map_container = create_filled_container(
129
  components=[
130
+ vm.Graph(
131
+ id="usa_map",
132
+ header="💡 Click on a state to filter ranked bars on the right.",
133
+ figure=regions_map_chart(df, column="Sales", custom_data=["State Code"]),
134
+ actions=va.set_control(control="state_filter", value="State Code"),
135
+ )
136
+ ],
137
+ )
138
+
139
+ regions_top_n_chart_container = create_filled_container(
140
+ components=[vm.Graph(id="regions_top_n_chart", figure=regions_top_n_chart(df, x="Sales", y="City", n=20))],
141
+ controls=[
142
+ vm.Parameter(
143
+ targets=["regions_top_n_chart.n"], selector=vm.Slider(min=5, max=30, step=5, value=20, title="Choose top N")
144
+ ),
145
+ vm.Parameter(
146
+ targets=["regions_top_n_chart.y"],
147
+ selector=vm.RadioItems(options=["City", "Customer Name", "Sub-Category"], title="Choose y-axis"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  ),
149
  ],
150
  )
151
 
152
+ regions_page = vm.Page(
153
+ title="Regions",
154
  components=[
155
  vm.Container(
156
  layout=vm.Grid(grid=[[0, 1]]),
157
+ components=[map_container, regions_top_n_chart_container],
158
  controls=[
159
+ vm.Filter(id="state_filter", column="State Code", visible=False),
160
  vm.Filter(column="Segment", selector=vm.Checklist(title="Choose segment")),
161
  vm.Filter(column="Category", selector=vm.Checklist(title="Choose category")),
162
+ vm.Parameter(
163
+ targets=["usa_map.column", "regions_top_n_chart.x"],
164
+ selector=vm.RadioItems(options=["Sales", "Profit"], title="Choose metric"),
 
 
 
 
 
 
 
165
  ),
 
166
  ],
167
+ )
168
+ ],
169
+ )
170
+
171
+ # TODO: highlight state instead of filter?
172
+ ###############################################################################
173
+ # Products page
174
+ ###############################################################################
175
+
176
+ product_category_container = create_filled_container(
177
+ components=[
178
+ vm.Graph(
179
+ id="product_category_bar",
180
+ figure=bar_chart_by_category(df, custom_data=["Category"]),
181
+ actions=va.set_control(control="product_category_filter", value="Category"),
182
+ )
183
+ ],
184
+ controls=[vm.Filter(id="product_category_filter", column="Category", visible=False)],
185
+ )
186
+
187
+ product_table_container = create_filled_container(
188
+ components=[
189
+ vm.AgGrid(
190
+ header="💡 Click on a row to highlight the data point in the matrix on the right.",
191
+ figure=dash_ag_grid(profit_df, columnDefs=COLUMN_DEFS_PRODUCT),
192
+ actions=va.set_control(control="quadrant_subcategory", value="Sub-Category"),
193
+ )
194
+ ],
195
+ )
196
+
197
+ profit_vs_sales_container = create_filled_container(
198
+ components=[
199
+ vm.Graph(
200
+ id="profit_vs_sales_chart",
201
+ figure=scatter_with_quadrants(
202
+ profit_df,
203
+ x="Sales",
204
+ y="Profit",
205
+ custom_data=["Sub-Category"],
206
+ ),
207
+ )
208
  ],
209
  controls=[
210
  vm.Parameter(
211
+ id="quadrant_subcategory",
212
+ targets=["profit_vs_sales_chart.highlight_sub_category"],
213
+ selector=vm.Dropdown(options=["NONE", *df["Sub-Category"]], multi=False),
214
  visible=False,
215
  ),
216
  ],
217
  )
218
 
219
+ # TODO: bar chart into histogram
220
+ # TODO: fix quadrant highlight
221
 
222
+ products_page = vm.Page(
223
  title="Products",
224
  components=[
225
  vm.Container(
226
+ layout=vm.Grid(grid=[[0, 2], [1, 2], [1, 2]]),
227
+ components=[product_category_container, product_table_container, profit_vs_sales_container],
228
+ )
229
+ ],
230
+ )
231
+
232
+
233
+ ###############################################################################
234
+ # Customers page
235
+ ###############################################################################
236
+
237
+ customers_page = vm.Page(
238
+ title="Customers",
239
+ components=[
240
+ vm.Container(
241
+ layout=vm.Grid(grid=[[0, 1]]),
242
  components=[
243
+ vm.AgGrid(
244
+ header="💡 Click on a row to highlight the customer.",
245
+ figure=customers_ag_grid(df),
246
+ actions=va.set_control(control="customer_parameter", value="Customer Name"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  ),
248
+ vm.Graph(id="pareto_chart", figure=pareto_customers_chart(df)),
249
+ ],
250
+ controls=[
251
+ vm.Filter(column="Region", selector=vm.Checklist(title="Choose region")),
252
+ vm.Filter(column="Segment", selector=vm.Checklist(title="Choose segment")),
253
+ vm.Filter(column="Category", selector=vm.Checklist(title="Choose category")),
254
+ vm.Parameter(
255
+ id="customer_parameter",
256
+ targets=["pareto_chart.highlight_customer"],
257
+ selector=vm.RadioItems(options=["NONE", *df["Customer Name"]]),
258
+ visible=False,
 
 
 
259
  ),
260
  ],
261
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  ],
263
  )
264
 
265
+ ###############################################################################
266
+ # Orders page
267
+ ###############################################################################
268
+
269
+ orders_page = vm.Page(
270
  title="Orders",
271
  layout=vm.Flex(),
272
  components=[
273
+ vm.AgGrid(figure=orders_ag_grid(df)),
274
+ vm.Button(text="Export data", icon="download", actions=va.export_data(file_format="xlsx")),
 
 
 
 
275
  ],
276
  )
277
 
278
+ ###############################################################################
279
+ # Overall dashboard configuration
280
+ ###############################################################################
281
+ pages = {
282
+ "Home": overview_page,
283
+ "Globe Asia": regions_page,
284
+ "Barcode": products_page,
285
+ "Group": customers_page,
286
+ "Shopping Cart": orders_page,
287
+ }
288
 
289
  navigation = vm.Navigation(
 
290
  nav_selector=vm.NavBar(
291
+ items=[vm.NavLink(pages=[page.id], label=page.title, icon=icon) for icon, page in pages.items()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  ),
293
  )
294
 
295
+ dashboard = vm.Dashboard(title="Superstore dashboard", pages=pages.values(), navigation=navigation, theme="vizro_light")
 
 
 
 
 
296
 
297
  app = Vizro().build(dashboard)
298
 
assets/{css/custom.css → custom.css} RENAMED
@@ -4,23 +4,7 @@
4
  --bs-pink: #e33b3b;
5
  }
6
 
7
- .flex-item .card-kpi {
8
- height: 150px;
9
- width: 300px;
10
- }
11
-
12
- #p3-filter-1 {
13
- display: none;
14
- }
15
-
16
- #p3-parameter-1 {
17
- display: none;
18
- }
19
-
20
- #pg2-filter-1,
21
- #pg2-filter-3 {
22
- display: none;
23
- }
24
 
25
  .card-kpi .card-header .material-symbols-outlined {
26
  font-size: 32px;
@@ -30,39 +14,31 @@
30
  font-size: 1.3rem;
31
  }
32
 
33
- #pg1-container-1_title_content {
34
  font-size: 14px;
35
  }
36
 
37
- #pg1-container-1.container-fluid {
38
  gap: 0;
39
  }
40
 
41
- #header {
42
- padding-left: 4px;
43
- }
44
 
 
 
 
45
 
46
- .form-label {
47
- font-weight: 600;
48
- }
49
 
50
- .container-controls-panel .dropdown {
51
- width: 360px;
52
- }
53
 
54
- .container-controls-panel {
55
- padding-bottom: 1rem;
56
- }
57
 
58
- .card-kpi:hover {
59
- border: 8px solid var(--bs-border);
60
- }
61
 
62
- .card-kpi:has(.color-pos):hover {
63
- border-left: 4px solid var(--bs-blue);
64
- }
65
-
66
- .card-kpi:has(.color-neg):hover {
67
- border-left: 4px solid var(--bs-pink);
68
- }
 
4
  --bs-pink: #e33b3b;
5
  }
6
 
7
+ /* KPI cards. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  .card-kpi .card-header .material-symbols-outlined {
10
  font-size: 32px;
 
14
  font-size: 1.3rem;
15
  }
16
 
17
+ #kpi-cards_title_content {
18
  font-size: 14px;
19
  }
20
 
21
+ #kpi-cards.container-fluid {
22
  gap: 0;
23
  }
24
 
25
+ /* TODO: do this part with callback to remain highlighted. */
 
 
26
 
27
+ /*.card-kpi:hover {*/
28
+ /* border: 8px solid var(--bs-border);*/
29
+ /*}*/
30
 
31
+ /*.card-kpi:has(.color-pos):hover {*/
32
+ /* border-left: 4px solid var(--bs-blue);*/
33
+ /*}*/
34
 
35
+ /*.card-kpi:has(.color-neg):hover {*/
36
+ /* border-left: 4px solid var(--bs-pink);*/
37
+ /*}*/
38
 
 
 
 
39
 
40
+ /* Font tweaks. */
 
 
41
 
42
+ .form-label {
43
+ font-weight: 600;
44
+ }
 
 
 
 
assets/{js/dashAgGridComponentFunctions.js → dashAgGridComponentFunctions.js} RENAMED
File without changes
charts.py CHANGED
@@ -1,9 +1,11 @@
1
  """Collection of custom charts."""
2
 
 
 
3
  import pandas as pd
4
  import plotly.graph_objects as go
5
  import vizro.plotly.express as px
6
- from dash_ag_grid import AgGrid
7
  from vizro.models.types import capture
8
 
9
  CURRENT_YEAR = 2017
@@ -13,6 +15,8 @@ PRIMARY_COLOR = "#2251ff"
13
  SECONDARY_COLOR = "#A0A2A8"
14
  ORANGE_COLOR = "#f6c343"
15
  GREEN_COLOR = "#60c96c"
 
 
16
  RED_COLOR = "#f17e7e"
17
  DIVERGING_RED_GREEN = [
18
  "#a84545",
@@ -37,121 +41,107 @@ DIVERGING_RED_BLUE = [
37
  "#051c2c", # highest value
38
  ]
39
 
 
40
 
41
- @capture("graph")
42
- def bar_chart_by_category(data_frame, custom_data):
43
- """Custom bar chart made with Plotly."""
44
- if not data_frame["Category"].eq(data_frame["Category"].iloc[0]).all():
45
- x = "Category"
46
- else:
47
- x = "Sub-Category"
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  fig = px.bar(
50
- data_frame,
51
- x=x,
52
- y="Sales",
53
- title=f"Sales | By {x} <br><sup> 💡 Click on the category to drill-down to sub-category. "
54
- f"Reset by using reset button next to the theme switch.</sup>",
55
- custom_data=custom_data,
56
- color_discrete_sequence=[PRIMARY_COLOR],
57
  )
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  fig.update_layout(
60
- yaxis={"visible": False},
61
- showlegend=False,
62
- bargap=0.6,
63
- xaxis_title=None,
64
  yaxis_title=None,
 
65
  )
66
-
67
  return fig
68
 
69
 
70
  @capture("graph")
71
- def create_map_bubble_new(data_frame, custom_data, value_col="Sales"):
72
- """Custom map chart made with Plotly."""
73
- if value_col == "Order ID":
74
- state_metric = (
75
- data_frame.groupby(["State_Code", "Region"], as_index=False)["Order ID"]
76
- .nunique()
77
- .rename(columns={"Order ID": "Orders"})
78
- )
79
- agg_col = "Orders"
80
- elif value_col == "Customer ID":
81
- state_metric = (
82
- data_frame.groupby(["State_Code", "Region"], as_index=False)["Customer ID"]
83
- .nunique()
84
- .rename(columns={"Customer ID": "Customers"})
85
- )
86
- agg_col = "Customers"
87
- else:
88
- state_metric = data_frame.groupby(["State_Code", "Region"], as_index=False)[value_col].sum()
89
- agg_col = value_col
90
 
91
- # Choropleth
92
- fig = px.choropleth(
93
- state_metric,
94
- locations="State_Code",
95
- locationmode="USA-states",
96
- color=agg_col,
97
- hover_name="Region",
98
- hover_data=["Region", agg_col],
99
- scope="usa",
100
- custom_data=custom_data,
101
- color_continuous_scale=DIVERGING_RED_BLUE,
102
- color_continuous_midpoint=0,
103
  )
104
 
105
  fig.update_layout(
106
- margin={"l": 10, "r": 10, "t": 10, "b": 10},
107
  )
108
- fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "bottom"}, "orientation": "h", "x": 0.5, "y": 0})
109
 
110
  return fig
111
 
112
 
113
  @capture("graph")
114
- def create_lollipop_chart_by_region(data_frame, value_col="Sales"):
115
- """Create a lollipop chart showing aggregated metrics by region using Plotly."""
116
- # --- Aggregate based on chosen metric ---
117
- if value_col == "Order ID":
118
- region_metric = (
119
- data_frame.groupby("Region", as_index=False)["Order ID"].nunique().rename(columns={"Order ID": "Orders"})
120
- )
121
- agg_col = "Orders"
122
- elif value_col == "Customer ID":
123
- region_metric = (
124
- data_frame.groupby("Region", as_index=False)["Customer ID"]
125
- .nunique()
126
- .rename(columns={"Customer ID": "Customers"})
127
- )
128
- agg_col = "Customers"
129
- else:
130
- region_metric = (
131
- data_frame.groupby("Region", as_index=False)[value_col].sum().rename(columns={value_col: value_col})
132
- )
133
- agg_col = value_col
134
-
135
- # --- Sort regions for visual clarity ---
136
- region_metric = region_metric.sort_values(by=agg_col, ascending=True)
137
-
138
- fig = go.Figure()
139
 
140
- fig.add_trace(
141
- go.Bar(
142
- x=region_metric[agg_col],
143
- y=region_metric["Region"],
144
- showlegend=False,
145
- hoverinfo="skip",
146
- orientation="h",
147
- marker={"color": PRIMARY_COLOR},
148
- )
149
  )
150
 
 
151
  fig.add_trace(
152
  go.Scatter(
153
- x=region_metric[agg_col],
154
- y=region_metric["Region"],
155
  mode="markers",
156
  marker={"size": 14, "color": PRIMARY_COLOR, "line": {"color": PRIMARY_COLOR, "width": 1.5}},
157
  showlegend=False,
@@ -159,244 +149,100 @@ def create_lollipop_chart_by_region(data_frame, value_col="Sales"):
159
  )
160
 
161
  fig.update_layout(
162
- title=f"{agg_col} | By Region",
163
  xaxis_title=None,
164
  yaxis_title=None,
165
  bargap=0.8,
 
166
  )
167
 
 
 
168
  return fig
169
 
170
 
171
  @capture("graph")
172
- def create_bar_current_vs_previous_segment(data_frame, value_col="Sales"):
173
- """Custom bar chart made with Plotly."""
174
- data_frame["Order Date"] = pd.to_datetime(data_frame["Order Date"])
175
- data_frame["Year"] = data_frame["Order Date"].dt.year
176
-
177
- if value_col == "Order ID":
178
- agg_df = (
179
- data_frame.groupby(["Segment", "Year"], as_index=False)["Order ID"]
180
- .nunique()
181
- .rename(columns={"Order ID": "Orders"})
182
- )
183
- agg_col = "Orders"
184
- elif value_col == "Customer ID":
185
- agg_df = (
186
- data_frame.groupby(["Segment", "Year"], as_index=False)["Customer ID"]
187
- .nunique()
188
- .rename(columns={"Customer ID": "Customers"})
189
- )
190
- agg_col = "Customers"
191
- else:
192
- agg_df = data_frame.groupby(["Segment", "Year"], as_index=False)[value_col].sum()
193
- agg_col = value_col
194
-
195
- pivot_df = agg_df.pivot_table(index="Segment", columns="Year", values=agg_col).reset_index()
196
-
197
- fig = go.Figure()
198
-
199
- if PREVIOUS_YEAR in pivot_df.columns:
200
- fig.add_trace(
201
- go.Bar(
202
- x=pivot_df["Segment"],
203
- y=pivot_df[PREVIOUS_YEAR],
204
- name="Previous year",
205
- marker_color=SECONDARY_COLOR,
206
- )
207
- )
208
-
209
- if CURRENT_YEAR in pivot_df.columns:
210
- fig.add_trace(
211
- go.Bar(
212
- x=pivot_df["Segment"],
213
- y=pivot_df[CURRENT_YEAR],
214
- name="Current year",
215
- marker_color=PRIMARY_COLOR,
216
- )
217
- )
218
-
219
- fig.update_layout(
220
- barmode="group",
221
- xaxis_title=None,
222
- yaxis_title=None,
223
- bargap=0.4,
224
- title=f"{agg_col} | By Customer Segment",
225
- showlegend=False,
226
- )
227
-
228
- return fig
229
 
230
 
231
  @capture("graph")
232
- def create_bar_current_vs_previous_category(data_frame, value_col="Sales"):
233
  """Bar chart comparing current year vs previous year by category."""
234
- data_frame["Year"] = data_frame["Order Date"].dt.year
235
 
236
- if value_col == "Order ID":
237
- agg_df = (
238
- data_frame.groupby(["Category", "Year"], as_index=False)["Order ID"]
239
- .nunique()
240
- .rename(columns={"Order ID": "Orders"})
241
- )
242
- agg_col = "Orders"
243
- elif value_col == "Customer ID":
244
- agg_df = (
245
- data_frame.groupby(["Category", "Year"], as_index=False)["Customer ID"]
246
- .nunique()
247
- .rename(columns={"Customer ID": "Customers"})
248
- )
249
- agg_col = "Customers"
250
- else:
251
- agg_df = data_frame.groupby(["Category", "Year"], as_index=False)[value_col].sum()
252
- agg_col = value_col
253
-
254
- pivot_df = agg_df.pivot_table(index="Category", columns="Year", values=agg_col).reset_index()
255
 
256
- fig = go.Figure()
257
-
258
- if PREVIOUS_YEAR in pivot_df.columns:
259
- fig.add_trace(
260
- go.Bar(
261
- x=pivot_df["Category"],
262
- y=pivot_df[PREVIOUS_YEAR],
263
- name="Previous year",
264
- marker_color=SECONDARY_COLOR,
265
- )
266
- )
267
 
268
- if CURRENT_YEAR in pivot_df.columns:
269
- fig.add_trace(
270
- go.Bar(
271
- x=pivot_df["Category"],
272
- y=pivot_df[CURRENT_YEAR],
273
- name="Current year",
274
- marker_color=PRIMARY_COLOR,
275
- )
276
- )
 
 
 
277
 
278
  fig.update_layout(
279
- barmode="group",
280
- xaxis_title=None,
281
- yaxis_title=None,
282
- bargap=0.4,
283
- title=f"{agg_col} | By Product Category",
284
- showlegend=False,
285
  )
 
286
 
287
  return fig
288
 
289
 
 
290
  @capture("graph")
291
- def create_line_chart_per_month(data_frame, value_col="Sales"):
292
- """Custom line chart made with Plotly."""
293
- data_frame["Order Date"] = pd.to_datetime(data_frame["Order Date"])
294
- data_frame["Year"] = data_frame["Order Date"].dt.year
295
- data_frame["Month"] = data_frame["Order Date"].dt.month
296
-
297
- if value_col == "Order ID":
298
- monthly = (
299
- data_frame.groupby(["Year", "Month"], as_index=False)["Order ID"]
300
- .nunique()
301
- .rename(columns={"Order ID": "Orders"})
302
- )
303
- agg_col = "Orders"
304
- elif value_col == "Customer ID":
305
- monthly = (
306
- data_frame.groupby(["Year", "Month"], as_index=False)["Customer ID"]
307
- .nunique()
308
- .rename(columns={"Customer ID": "Customers"})
309
- )
310
- agg_col = "Customers"
311
- else:
312
- monthly = data_frame.groupby(["Year", "Month"], as_index=False)[value_col].sum()
313
- agg_col = value_col
314
-
315
- prev_year = monthly[monthly["Year"] == PREVIOUS_YEAR]
316
- curr_year = monthly[monthly["Year"] == CURRENT_YEAR]
317
-
318
- fig = go.Figure()
319
- fig.add_trace(
320
- go.Scatter(
321
- x=curr_year["Month"],
322
- y=curr_year[agg_col],
323
- name="Current Year",
324
- marker_color=PRIMARY_COLOR,
325
- line_width=2,
326
- fill="tozeroy",
327
- )
328
- )
329
- fig.add_trace(
330
- go.Scatter(
331
- x=prev_year["Month"],
332
- y=prev_year[agg_col],
333
- name="Previous Year",
334
- marker_color=SECONDARY_COLOR,
335
- )
336
- )
337
-
338
- fig.update_layout(
339
- xaxis={
340
- "showgrid": False,
341
- "title": None,
342
- "tickmode": "array",
343
- "tickvals": list(range(1, 13)),
344
- "range": [1, 12],
345
- "ticktext": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
346
- },
347
- yaxis_title=None,
348
- title=f"{agg_col} | By Month",
349
- legend={"yanchor": "top", "y": 1.2, "xanchor": "right", "x": 1},
350
- )
351
-
352
  return fig
353
 
354
 
 
355
  @capture("graph")
356
- def pie_chart_by_order_status(data_frame, value_col="Sales"):
357
- """Pie chart showing distribution by Order Status."""
358
- if value_col == "Order ID":
359
- status_metric = (
360
- data_frame.groupby("Order Status", as_index=False)["Order ID"]
361
- .nunique()
362
- .rename(columns={"Order ID": "Orders"})
363
- )
364
- agg_col = "Orders"
365
- elif value_col == "Customer ID":
366
- status_metric = (
367
- data_frame.groupby("Order Status", as_index=False)["Customer ID"]
368
- .nunique()
369
- .rename(columns={"Customer ID": "Customers"})
370
- )
371
- agg_col = "Customers"
372
  else:
373
- status_metric = data_frame.groupby("Order Status", as_index=False)[value_col].sum()
374
- agg_col = value_col
375
 
376
- fig = px.pie(
377
- status_metric,
378
- names="Order Status",
379
- values=agg_col,
380
- color="Order Status",
381
- title=f"{agg_col} by Order Status",
382
- color_discrete_map={"In Transit": PRIMARY_COLOR, "Processing": ORANGE_COLOR, "Delivered": GREEN_COLOR},
383
- hole=0.6,
384
  )
385
 
386
  fig.update_layout(
387
- title=f"{agg_col} | By Order Status",
388
- legend={"yanchor": "bottom", "y": -0.2, "xanchor": "right", "orientation": "v"},
 
 
 
389
  )
390
 
391
  return fig
392
 
393
 
 
394
  @capture("graph")
395
  def scatter_with_quadrants(
 
396
  x: str,
397
  y: str,
398
  custom_data: list[str],
399
- data_frame: pd.DataFrame = None,
400
  highlight_sub_category=None,
401
  ):
402
  """Custom scatter plot with quadrants grouped by Sub-Category."""
@@ -514,304 +360,49 @@ def scatter_with_quadrants(
514
 
515
 
516
  @capture("graph")
517
- def pareto_customers_chart(data_frame, value_col="Sales", highlight_customer=None):
518
- """Custom chart made with Plotly."""
519
- customer_df = (
520
- data_frame.groupby("Customer Name", as_index=False)[value_col].sum().sort_values(by=value_col, ascending=False)
 
 
 
 
 
521
  )
522
-
523
- customer_df["Cumulative Value"] = customer_df[value_col].cumsum()
524
- total_value = customer_df[value_col].sum()
525
- customer_df["Cumulative % Value"] = 100 * customer_df["Cumulative Value"] / total_value
526
-
527
- customer_df["Customer Rank"] = range(1, len(customer_df) + 1)
528
- customer_df["Cumulative % Customers"] = 100 * customer_df["Customer Rank"] / len(customer_df)
529
-
530
- thresholds = {
531
- "A": 20,
532
- "B": 50,
533
- "C": 100,
534
- }
535
-
536
- def assign_segment(cust_pct):
537
- if cust_pct <= thresholds["A"]:
538
- return "A"
539
- elif cust_pct <= thresholds["B"]:
540
- return "B"
541
- else:
542
- return "C"
543
-
544
- customer_df["Segment"] = customer_df["Cumulative % Customers"].apply(assign_segment)
545
 
546
  fig = px.line(
547
- customer_df,
548
- x="Cumulative % Customers",
549
- y="Cumulative % Value",
550
  markers=True,
551
- title=f"Pareto Analysis of Customers ({value_col})",
552
- hover_data=["Customer Name", value_col, "Cumulative % Value"],
553
  color_discrete_sequence=[ORANGE_COLOR],
554
  )
555
- fig.update_traces(showlegend=False)
556
- fig.add_vrect(x0=0, x1=thresholds["A"], fillcolor=PRIMARY_COLOR, opacity=0.6, layer="below")
557
- fig.add_vrect(x0=thresholds["A"], x1=thresholds["B"], fillcolor=PRIMARY_COLOR, opacity=0.3, layer="below")
558
- fig.add_vrect(x0=thresholds["B"], x1=100, fillcolor=PRIMARY_COLOR, opacity=0.1, layer="below")
559
-
560
- if highlight_customer and highlight_customer in customer_df["Customer Name"].to_numpy():
561
- cust = customer_df[customer_df["Customer Name"] == highlight_customer].iloc[0]
562
- fig.add_trace(
563
- go.Scatter(
564
- x=[cust["Cumulative % Customers"]],
565
- y=[cust["Cumulative % Value"]],
566
  mode="markers+text",
567
- text=[highlight_customer],
568
  textposition="top right",
569
- marker={"color": ORANGE_COLOR, "size": 12, "line": {"width": 2}},
570
- showlegend=False,
571
  )
572
- )
573
-
574
- fig.update_layout(
575
- xaxis_title="% of Total Customers",
576
- yaxis_title=f"Cumulative % of {value_col}",
577
- yaxis={"range": [0, 105]},
578
- xaxis={"range": [0, 105]},
579
- )
580
-
581
- return fig
582
 
 
 
583
 
584
- CELL_STYLE_PRODUCT = {
585
- "styleConditions": [
586
- {
587
- "condition": "params.value < -0.5",
588
- "style": {"backgroundColor": "#e33b3b"},
589
- },
590
- {
591
- "condition": "params.value >= -0.5 && params.value <= 0",
592
- "style": {"backgroundColor": "#f19791"},
593
- },
594
- {
595
- "condition": "params.value > 0 && params.value <= 0.30",
596
- "style": {"backgroundColor": "#728aff"},
597
- },
598
- {
599
- "condition": "params.value > 0.30",
600
- "style": {"backgroundColor": "#2251ff"},
601
- },
602
- ]
603
- }
604
-
605
-
606
- COLUMN_DEFS_PRODUCT = [
607
- # {"field": "Product Name", "cellDataType": "text", "headerName": "Product", "flex": 3},
608
- {"field": "Sub-Category", "cellDataType": "text", "headerName": "Sub-Category", "flex": 3},
609
- {
610
- "field": "Profit",
611
- "cellDataType": "number",
612
- "flex": 2,
613
- "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
614
- },
615
- {
616
- "field": "Sales",
617
- "cellDataType": "number",
618
- "flex": 2,
619
- "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
620
- },
621
- {
622
- "field": "Profit Margin",
623
- "flex": 2,
624
- "cellDataType": "number",
625
- "valueFormatter": {"function": "d3.format('.0%')(params.value)"},
626
- "cellStyle": CELL_STYLE_PRODUCT,
627
- },
628
- ]
629
-
630
-
631
- CELL_STYLE_CUSTOMERS = {
632
- "styleConditions": [
633
- {
634
- "condition": "params.value < -0.1",
635
- "style": {"backgroundColor": "#ff9222"},
636
- },
637
- {
638
- "condition": "params.value >= -0.1 && params.value <= 0",
639
- "style": {"backgroundColor": "#ffba7f"},
640
- },
641
- {
642
- "condition": "params.value > 0 && params.value <= 0.05",
643
- "style": {"backgroundColor": "#e4e4e4"},
644
- },
645
- {
646
- "condition": "params.value > 0.05 && params.value <= 0.15",
647
- "style": {"backgroundColor": "#b7d4ee"},
648
- },
649
- {
650
- "condition": "params.value > 0.15 && params.value <= 0.20",
651
- "style": {"backgroundColor": "#80c4f6"},
652
- },
653
- {
654
- "condition": "params.value > 0.20",
655
- "style": {"backgroundColor": "#00b4ff"},
656
- },
657
- ]
658
- }
659
-
660
-
661
- COLUMN_DEFS_CUSTOMERS = [
662
- {"field": "Rank", "cellDataType": "number", "headerName": "Rank", "flex": 2},
663
- {
664
- "field": "Customer Name",
665
- "cellDataType": "text",
666
- "flex": 3,
667
- # "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
668
- },
669
- {
670
- "field": "Sales",
671
- "cellDataType": "number",
672
- "flex": 3,
673
- "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
674
- },
675
- {
676
- "headerName": "Cumulative %",
677
- "field": "Cumulative %",
678
- "type": "numericColumn",
679
- "valueFormatter": {"function": "d3.format('.1f')(params.value) + '%'"},
680
- "width": 130,
681
- },
682
- ]
683
-
684
 
685
- @capture("graph")
686
- def bar_chart_top_n(data_frame, x="Sales", y="City", top_n=10):
687
- """Generic bar chart to show top N by any dimension."""
688
- # TODO: Needs to be fixed when prefiltered on city. Otherwise, top N is not properly recalculated
689
- # after clicking on a state.
690
- df_top = data_frame.groupby(y).agg({x: "sum"}).sort_values(x, ascending=False).head(top_n).reset_index()
691
-
692
- # Sort ascending so highest appears at top in horizontal bar chart
693
- df_top = df_top.sort_values(x, ascending=True)
694
-
695
- # Create bar chart
696
- fig = px.bar(
697
- df_top,
698
- x=x,
699
- y=y,
700
- orientation="h",
701
- color_discrete_sequence=[PRIMARY_COLOR],
702
- title=f"Top {top_n} {y} by {x}",
703
- )
704
 
705
  return fig
706
-
707
-
708
- @capture("ag_grid")
709
- def custom_orders_aggrid(data_frame):
710
- """Custom aggrid table."""
711
- data_frame["Profit Ratio"] = (data_frame["Profit"] / data_frame["Sales"]).round(3)
712
- column_defs_orders = [
713
- {"headerName": "Order ID", "field": "Order ID", "minWidth": 150},
714
- {"headerName": "Status", "field": "Order Status", "minWidth": 150, "cellRenderer": "statusCellRenderer"},
715
- {
716
- "headerName": "Segment",
717
- "field": "Segment",
718
- "minWidth": 140,
719
- },
720
- {"headerName": "Customer", "field": "Customer Name", "minWidth": 170},
721
- {"headerName": "State", "field": "State", "minWidth": 150},
722
- {"headerName": "City", "field": "City", "minWidth": 150},
723
- {"headerName": "Category", "field": "Category", "minWidth": 150},
724
- {"headerName": "Sub-Category", "field": "Sub-Category", "minWidth": 150},
725
- {"headerName": "Sales", "field": "Sales", "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"}},
726
- {
727
- "headerName": "Profit",
728
- "field": "Profit",
729
- "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
730
- },
731
- {
732
- "headerName": "Profit Ratio",
733
- "field": "Profit Ratio",
734
- "minWidth": 140,
735
- "valueFormatter": {"function": "d3.format('.1%')(params.value)"},
736
- "cellStyle": {
737
- "styleConditions": [
738
- {
739
- "condition": "Number(params.value) < -0.5",
740
- "style": {
741
- "backgroundColor": "#e33b3b",
742
- "color": "white",
743
- "borderRadius": "18px",
744
- "padding": "4px",
745
- "fontWeight": "600",
746
- "justifyContent": "center",
747
- "alignItems": "center",
748
- "display": "flex",
749
- "marginTop": "8px",
750
- "height": "30px",
751
- },
752
- },
753
- {
754
- "condition": "Number(params.value) >= -0.5 && Number(params.value) < 0",
755
- "style": {
756
- "backgroundColor": "#f19791",
757
- "color": "white",
758
- "borderRadius": "18px",
759
- "padding": "4px",
760
- "fontWeight": "600",
761
- "justifyContent": "center",
762
- "alignItems": "center",
763
- "display": "flex",
764
- "marginTop": "8px",
765
- "height": "30px",
766
- },
767
- },
768
- {
769
- "condition": "Number(params.value) >= 0 && Number(params.value) < 0.30",
770
- "style": {
771
- "backgroundColor": "#728aff",
772
- "color": "white",
773
- "borderRadius": "18px",
774
- "padding": "4px",
775
- "fontWeight": "600",
776
- "justifyContent": "center",
777
- "alignItems": "center",
778
- "display": "flex",
779
- "marginTop": "8px",
780
- "height": "30px",
781
- },
782
- },
783
- {
784
- "condition": "Number(params.value) >= 0.30",
785
- "style": {
786
- "backgroundColor": "#2251ff",
787
- "color": "white",
788
- "borderRadius": "18px",
789
- "padding": "4px",
790
- "fontWeight": "600",
791
- "justifyContent": "center",
792
- "alignItems": "center",
793
- "display": "flex",
794
- "marginTop": "8px",
795
- "height": "30px",
796
- },
797
- },
798
- ]
799
- },
800
- },
801
- ]
802
-
803
- aggrid = AgGrid(
804
- columnDefs=column_defs_orders,
805
- defaultColDef={"resizable": True, "sortable": True, "filter": True, "minWidth": 30, "flex": 1},
806
- style={"height": "800px", "width": "100%"},
807
- rowData=data_frame.to_dict("records"),
808
- dashGridOptions={
809
- "rowHeight": 55,
810
- "animateRows": True,
811
- "suppressMovableColumns": True,
812
- "pagination": True,
813
- "paginationPageSize": 20,
814
- },
815
- dangerously_allow_code=True,
816
- )
817
- return aggrid
 
1
  """Collection of custom charts."""
2
 
3
+ import calendar
4
+
5
  import pandas as pd
6
  import plotly.graph_objects as go
7
  import vizro.plotly.express as px
8
+ from data_processing import COLUMN_TO_AGGFUNC, COLUMN_TO_METRIC, LAST_YEAR, THIS_YEAR, make_customer_sales_pareto_df
9
  from vizro.models.types import capture
10
 
11
  CURRENT_YEAR = 2017
 
15
  SECONDARY_COLOR = "#A0A2A8"
16
  ORANGE_COLOR = "#f6c343"
17
  GREEN_COLOR = "#60c96c"
18
+
19
+ YEAR_COLOR_MAP = {"This year": PRIMARY_COLOR, "Last year": SECONDARY_COLOR}
20
  RED_COLOR = "#f17e7e"
21
  DIVERGING_RED_GREEN = [
22
  "#a84545",
 
41
  "#051c2c", # highest value
42
  ]
43
 
44
+ # TODO: modify Vizro chart template instead of putting colors here? Put colors in colors SimpleNamespace?
45
 
46
+
47
+ # Helper functions
48
+ def _aggregate_data(data_frame, group_columns, column):
49
+ """Aggregate data using the appropriate function for the column."""
50
+ return data_frame.groupby(group_columns, as_index=False).agg({column: COLUMN_TO_AGGFUNC[column]})
51
+
52
+
53
+ # TODO: move to data_processing?
54
+ def _relabel_years(data_frame):
55
+ """Relabel years for human-readable plotting."""
56
+ data_frame["Year"] = data_frame["Year"].map({THIS_YEAR: "This year", LAST_YEAR: "Last year"})
57
+ return data_frame
58
+
59
+
60
+ def _create_year_comparison_bar_chart(data_frame, group_column, column, title_suffix):
61
+ """Helper function to create a grouped bar chart comparing years."""
62
+ grouped_df = _aggregate_data(data_frame, [group_column, "Year"], column)
63
+ grouped_df = _relabel_years(grouped_df)
64
 
65
  fig = px.bar(
66
+ grouped_df,
67
+ x=group_column,
68
+ y=column,
69
+ color="Year",
70
+ barmode="group",
71
+ title=f"{COLUMN_TO_METRIC[column]} | {title_suffix}",
72
+ color_discrete_map=YEAR_COLOR_MAP,
73
  )
74
 
75
+ fig.update_layout(xaxis_title=None, yaxis_title=None, bargap=0.4, showlegend=False)
76
+
77
+ return fig
78
+
79
+
80
+ # Chart functions in order of usage in app.py
81
+ @capture("graph")
82
+ def overview_by_month(data_frame, column):
83
+ grouped_df = _aggregate_data(data_frame, ["Year", "Month"], column)
84
+ # Relabel for plotting in human-readable way.
85
+ grouped_df["Month"] = grouped_df["Month"].map({i: calendar.month_abbr[i] for i in range(1, 13)})
86
+ grouped_df = _relabel_years(grouped_df)
87
+
88
+ fig = px.line(
89
+ grouped_df,
90
+ x="Month",
91
+ color="Year",
92
+ y=column,
93
+ markers=True,
94
+ title=f"{COLUMN_TO_METRIC[column]} | By Month",
95
+ color_discrete_map=YEAR_COLOR_MAP,
96
+ )
97
+ fig.data[1].update(line_width=2, fill="tozeroy")
98
  fig.update_layout(
99
+ xaxis={"showgrid": False, "title": None, "range": [-0.1, 11.1]},
 
 
 
100
  yaxis_title=None,
101
+ legend={"yanchor": "top", "y": 1.2, "xanchor": "right", "x": 1, "title": None},
102
  )
 
103
  return fig
104
 
105
 
106
  @capture("graph")
107
+ def overview_by_order_status(data_frame, column):
108
+ grouped_df = _aggregate_data(data_frame, "Order Status", column)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ fig = px.pie(
111
+ grouped_df,
112
+ names="Order Status",
113
+ values=column,
114
+ color="Order Status",
115
+ title=f"{COLUMN_TO_METRIC[column]} | By Order Status",
116
+ color_discrete_map={"In Transit": PRIMARY_COLOR, "Processing": ORANGE_COLOR, "Delivered": GREEN_COLOR},
117
+ hole=0.6,
 
 
 
 
118
  )
119
 
120
  fig.update_layout(
121
+ legend={"yanchor": "bottom", "y": -0.2, "xanchor": "right", "orientation": "v"},
122
  )
 
123
 
124
  return fig
125
 
126
 
127
  @capture("graph")
128
+ def overview_by_region(data_frame, column):
129
+ grouped_df = _aggregate_data(data_frame, "Region", column).sort_values(column, ascending=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ fig = px.bar(
132
+ grouped_df,
133
+ x=column,
134
+ y="Region",
135
+ orientation="h",
136
+ title=f"{COLUMN_TO_METRIC[column]} | By Region",
137
+ color_discrete_sequence=[PRIMARY_COLOR],
 
 
138
  )
139
 
140
+ # Add scatter markers on top to create lollipop effect
141
  fig.add_trace(
142
  go.Scatter(
143
+ x=grouped_df[column],
144
+ y=grouped_df["Region"],
145
  mode="markers",
146
  marker={"size": 14, "color": PRIMARY_COLOR, "line": {"color": PRIMARY_COLOR, "width": 1.5}},
147
  showlegend=False,
 
149
  )
150
 
151
  fig.update_layout(
 
152
  xaxis_title=None,
153
  yaxis_title=None,
154
  bargap=0.8,
155
+ showlegend=False,
156
  )
157
 
158
+ fig.update_traces(hoverinfo="skip", selector={"type": "bar"})
159
+
160
  return fig
161
 
162
 
163
  @capture("graph")
164
+ def overview_by_customer_segment(data_frame, column):
165
+ """Bar chart comparing current year vs previous year by customer segment."""
166
+ return _create_year_comparison_bar_chart(data_frame, "Segment", column, "By Customer Segment")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
 
169
  @capture("graph")
170
+ def overview_by_product_category(data_frame, column):
171
  """Bar chart comparing current year vs previous year by category."""
172
+ return _create_year_comparison_bar_chart(data_frame, "Category", column, "By Product Category")
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ # TODO: label with dollars, change color scale depending on metric.
176
+ @capture("graph")
177
+ def regions_map_chart(data_frame, custom_data, column):
178
+ grouped_df = _aggregate_data(data_frame, ["State Code", "Region"], column)
 
 
 
 
 
 
 
179
 
180
+ fig = px.choropleth(
181
+ grouped_df,
182
+ locations="State Code",
183
+ locationmode="USA-states",
184
+ color=column,
185
+ hover_name="Region",
186
+ hover_data=column,
187
+ scope="usa",
188
+ custom_data=custom_data,
189
+ color_continuous_scale=DIVERGING_RED_BLUE,
190
+ color_continuous_midpoint=0,
191
+ )
192
 
193
  fig.update_layout(
194
+ margin={"l": 10, "r": 10, "t": 10, "b": 10},
 
 
 
 
 
195
  )
196
+ fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "bottom"}, "orientation": "h", "x": 0.5, "y": 0})
197
 
198
  return fig
199
 
200
 
201
+ # TODO: label with dollars
202
  @capture("graph")
203
+ def regions_top_n_chart(data_frame, x, y, n):
204
+ top_df = data_frame.groupby(y).agg({x: COLUMN_TO_AGGFUNC[x]}).sort_values(x).head(n).reset_index()
205
+ fig = px.bar(top_df, x=x, y=y, orientation="h", color_discrete_sequence=[PRIMARY_COLOR], title=f"Top {n} by {x}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  return fig
207
 
208
 
209
+ # TODO: check and refactor
210
  @capture("graph")
211
+ def bar_chart_by_category(data_frame, custom_data):
212
+ """Custom bar chart made with Plotly."""
213
+ if not data_frame["Category"].eq(data_frame["Category"].iloc[0]).all():
214
+ x = "Category"
 
 
 
 
 
 
 
 
 
 
 
 
215
  else:
216
+ x = "Sub-Category"
 
217
 
218
+ fig = px.bar(
219
+ data_frame,
220
+ x=x,
221
+ y="Sales",
222
+ title=f"Sales | By {x} <br><sup> 💡 Click on the category to drill-down to sub-category. "
223
+ f"Reset by using reset button next to the theme switch.</sup>",
224
+ custom_data=custom_data,
225
+ color_discrete_sequence=[PRIMARY_COLOR],
226
  )
227
 
228
  fig.update_layout(
229
+ yaxis={"visible": False},
230
+ showlegend=False,
231
+ bargap=0.6,
232
+ xaxis_title=None,
233
+ yaxis_title=None,
234
  )
235
 
236
  return fig
237
 
238
 
239
+ # TODO: check and refactor
240
  @capture("graph")
241
  def scatter_with_quadrants(
242
+ data_frame: pd.DataFrame,
243
  x: str,
244
  y: str,
245
  custom_data: list[str],
 
246
  highlight_sub_category=None,
247
  ):
248
  """Custom scatter plot with quadrants grouped by Sub-Category."""
 
360
 
361
 
362
  @capture("graph")
363
+ def pareto_customers_chart(data_frame, highlight_customer=None):
364
+ # Need to run make_customer_sales_pareto_df after Filters are applied, hence it's inside this custom chart
365
+ # rather than passing a pareto df into this function.
366
+ data_frame = make_customer_sales_pareto_df(data_frame)
367
+ x, y = "% of Total Customers", "Cumulative % of Sales"
368
+
369
+ # Add origin point (0, 0) to the start of the dataframe so line joins to origin.
370
+ origin_row = pd.DataFrame(
371
+ {x: [0], y: [0], "Customer Name": [""], "Sales": [0], "Cumulative Sales": [0], "Rank": [0]}
372
  )
373
+ data_frame = pd.concat([origin_row, data_frame], ignore_index=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  fig = px.line(
376
+ data_frame,
377
+ x=x,
378
+ y=y,
379
  markers=True,
380
+ title="Pareto Analysis of Customer Sales",
 
381
  color_discrete_sequence=[ORANGE_COLOR],
382
  )
383
+
384
+ # Add another trace for the highlighted customers.
385
+ if highlight_customer is not None:
386
+ highlight_customer_data = data_frame[data_frame["Customer Name"] == highlight_customer]
387
+ if not highlight_customer_data.empty:
388
+ highlight_customer_data = highlight_customer_data.iloc[0]
389
+ fig.add_scatter(
390
+ x=[highlight_customer_data[x]],
391
+ y=[highlight_customer_data[y]],
 
 
392
  mode="markers+text",
 
393
  textposition="top right",
394
+ text=[highlight_customer_data["Customer Name"]],
395
+ marker=dict(size=12, color=ORANGE_COLOR, line=dict(width=2)),
396
  )
 
 
 
 
 
 
 
 
 
 
397
 
398
+ thresholds = [0, 20, 50, 100]
399
+ opacities = [0.6, 0.3, 0.1]
400
 
401
+ # Add coloured background for different thresholds.
402
+ for (x0, x1), opacity in zip(zip(thresholds, thresholds[1:]), opacities):
403
+ fig.add_shape(type="rect", x0=x0, x1=x1, opacity=opacity)
404
+ fig.update_shapes(y0=0, y1=100, fillcolor=PRIMARY_COLOR, layer="below", line_width=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
+ fig.update_layout(showlegend=False, yaxis={"range": [0, 102]}, xaxis={"range": [0, 102]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
  return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data.py DELETED
@@ -1,119 +0,0 @@
1
- """Data for custom charts."""
2
-
3
- import random
4
-
5
- import pandas as pd
6
-
7
- # Map state names to state codes
8
- state_code_map = {
9
- "Alabama": "AL",
10
- "Alaska": "AK",
11
- "Arizona": "AZ",
12
- "Arkansas": "AR",
13
- "California": "CA",
14
- "Colorado": "CO",
15
- "Connecticut": "CT",
16
- "Delaware": "DE",
17
- "Florida": "FL",
18
- "Georgia": "GA",
19
- "Hawaii": "HI",
20
- "Idaho": "ID",
21
- "Illinois": "IL",
22
- "Indiana": "IN",
23
- "Iowa": "IA",
24
- "Kansas": "KS",
25
- "Kentucky": "KY",
26
- "Louisiana": "LA",
27
- "Maine": "ME",
28
- "Maryland": "MD",
29
- "Massachusetts": "MA",
30
- "Michigan": "MI",
31
- "Minnesota": "MN",
32
- "Mississippi": "MS",
33
- "Missouri": "MO",
34
- "Montana": "MT",
35
- "Nebraska": "NE",
36
- "Nevada": "NV",
37
- "New Hampshire": "NH",
38
- "New Jersey": "NJ",
39
- "New Mexico": "NM",
40
- "New York": "NY",
41
- "North Carolina": "NC",
42
- "North Dakota": "ND",
43
- "Ohio": "OH",
44
- "Oklahoma": "OK",
45
- "Oregon": "OR",
46
- "Pennsylvania": "PA",
47
- "Rhode Island": "RI",
48
- "South Carolina": "SC",
49
- "South Dakota": "SD",
50
- "Tennessee": "TN",
51
- "Texas": "TX",
52
- "Utah": "UT",
53
- "Vermont": "VT",
54
- "Virginia": "VA",
55
- "Washington": "WA",
56
- "West Virginia": "WV",
57
- "Wisconsin": "WI",
58
- "Wyoming": "WY",
59
- }
60
-
61
- superstore_df = pd.read_csv("superstore.csv", encoding="latin1")
62
-
63
- superstore_df["State_Code"] = superstore_df["State"].map(state_code_map)
64
-
65
- superstore_df["Order Date"] = pd.to_datetime(superstore_df["Order Date"], errors="coerce")
66
- superstore_df["Ship Date"] = pd.to_datetime(superstore_df["Ship Date"], errors="coerce")
67
-
68
- superstore_df["Year"] = superstore_df["Order Date"].dt.year
69
- superstore_df["Month"] = superstore_df["Order Date"].dt.month
70
- superstore_df["Day"] = superstore_df["Order Date"].dt.day
71
-
72
- # Find the latest 2 years in the dataset
73
- latest_two_years = sorted(superstore_df["Year"].unique())[-2:]
74
-
75
- # Filter dataframe for only those two years
76
- superstore_df = superstore_df[superstore_df["Year"].isin(latest_two_years)].copy()
77
-
78
- # Create Order Status - randomly assign one of three status values
79
-
80
- random.seed(42) # For reproducibility
81
- superstore_df["Order Status"] = superstore_df.apply(
82
- lambda x: random.choice(["Delivered", "In Transit", "Processing"]), axis=1
83
- )
84
-
85
-
86
- def create_superstore_product(data_frame):
87
- """Creates pandas data frame."""
88
- # data_frame["Category / Sub-Category"] = data_frame["Category"] + " / " + data_frame["Sub-Category"]
89
- data_frame = data_frame.groupby(["Sub-Category"]).agg({"Sales": "sum", "Profit": "sum"}).reset_index()
90
- data_frame["Profit Margin"] = data_frame["Profit"] / data_frame["Sales"]
91
- data_frame["Profit Absolute"] = abs(data_frame["Profit"])
92
-
93
- return data_frame
94
-
95
-
96
- def pareto_customers_table(data_frame):
97
- """Creates pandas data frame."""
98
- df = data_frame.groupby("Customer Name")["Sales"].sum().reset_index().sort_values(by="Sales", ascending=False)
99
-
100
- df["Cumulative Sales"] = df["Sales"].cumsum()
101
- df["Cumulative %"] = 100 * df["Cumulative Sales"] / df["Sales"].sum()
102
- df["Rank"] = range(1, len(df) + 1)
103
-
104
- return df[["Rank", "Customer Name", "Sales", "Cumulative Sales", "Cumulative %"]]
105
-
106
-
107
- def create_kpi_data(df, value_col="Sales"):
108
- """Creates pandas data frame."""
109
- df["Year"] = df["Order Date"].dt.year
110
- # sales_by_year = df.groupby('Year')[value_col].sum()
111
-
112
- if value_col in ["Order ID", "Customer ID"]:
113
- result_by_year = df.groupby("Year")[value_col].nunique()
114
- else:
115
- result_by_year = df.groupby("Year")[value_col].sum()
116
-
117
- new_df = pd.DataFrame({"total_2016": [result_by_year.get(2016, 0)], "total_2017": [result_by_year.get(2017, 0)]})
118
-
119
- return new_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data_processing.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data processing functions for the Superstore BI dashboard."""
2
+
3
+ import random
4
+
5
+ import pandas as pd
6
+ import us
7
+
8
+ THIS_YEAR = 2017
9
+ LAST_YEAR = 2016
10
+ COLUMN_TO_METRIC = {"Sales": "Sales", "Profit": "Profit", "Order ID": "Orders", "Customer ID": "Customers"}
11
+ COLUMN_TO_AGGFUNC = {"Sales": "sum", "Profit": "sum", "Order ID": "nunique", "Customer ID": "nunique"}
12
+
13
+
14
+ def make_superstore_df():
15
+ """Load and preprocess the Superstore dataset.
16
+
17
+ Returns:
18
+ pd.DataFrame: Processed dataframe with state codes, filtered to latest 2 years,
19
+ and enriched with Year, Month, and Order Status columns.
20
+ """
21
+ df = pd.read_csv("superstore.csv", encoding="latin1", parse_dates=["Order Date", "Ship Date"])
22
+
23
+ # Map state names to state codes using us library. Needed for px.choropleth.
24
+ df["State Code"] = df["State"].map({state.name: state.abbr for state in us.states.STATES})
25
+
26
+ # Filter dataframe for only the latest 2 years and add a month column.
27
+ df["Year"] = df["Order Date"].dt.year
28
+ df["Month"] = df["Order Date"].dt.month
29
+ df = df[df["Year"].isin([THIS_YEAR, LAST_YEAR])]
30
+
31
+ # Create order status - randomly assign with weights: 60% Delivered, 10% In Transit, 30% Processing
32
+ df["Order Status"] = random.choices(["Delivered", "In Transit", "Processing"], weights=[0.6, 0.1, 0.3], k=len(df))
33
+ return df
34
+
35
+
36
+ def make_superstore_profit_df(df):
37
+ """Aggregate product data by sub-category with profit metrics.
38
+
39
+ Args:
40
+ df: Source dataframe containing Sales and Profit columns.
41
+
42
+ Returns:
43
+ pd.DataFrame: Aggregated dataframe with Sub-Category, Sales, Profit,
44
+ Profit Margin, and Profit Absolute columns.
45
+ """
46
+ df = df.groupby("Sub-Category", as_index=False).agg({"Sales": "sum", "Profit": "sum"})
47
+ df["Profit Margin"] = df["Profit"] / df["Sales"]
48
+ df["Profit Absolute"] = df["Profit"].abs()
49
+ return df
50
+
51
+
52
+ def make_customer_sales_pareto_df(df):
53
+ """Create Pareto analysis dataframe for customer sales.
54
+
55
+ Args:
56
+ df: Source dataframe containing Customer Name and Sales columns.
57
+
58
+ Returns:
59
+ pd.DataFrame: Customer sales ranked with cumulative totals and percentages.
60
+ """
61
+ df = df.groupby("Customer Name", as_index=False)["Sales"].sum().sort_values("Sales", ascending=False)
62
+ df["Cumulative Sales"] = df["Sales"].cumsum()
63
+ df["Cumulative % of Sales"] = 100 * df["Cumulative Sales"] / df["Sales"].sum()
64
+ df["Rank"] = range(1, len(df) + 1)
65
+ df["% of Total Customers"] = 100 * df["Rank"] / len(df)
66
+ return df[["Rank", "Customer Name", "Sales", "Cumulative Sales", "Cumulative % of Sales", "% of Total Customers"]]
requirements.in CHANGED
@@ -1,4 +1,4 @@
1
  gunicorn
2
  openpyxl
3
  vizro==0.1.47
4
- numpy!=2.0.2 # there is an issue with this specific version that the whl can't be built
 
1
  gunicorn
2
  openpyxl
3
  vizro==0.1.47
4
+ us
requirements.txt CHANGED
@@ -43,11 +43,11 @@ gunicorn==23.0.0
43
  idna==3.11
44
  # via requests
45
  importlib-metadata==8.7.0
46
- # via
47
- # dash
48
- # flask
49
  itsdangerous==2.2.0
50
  # via flask
 
 
51
  jinja2==3.1.6
52
  # via flask
53
  markupsafe==3.0.3
@@ -62,9 +62,7 @@ narwhals==2.10.0
62
  nest-asyncio==1.6.0
63
  # via dash
64
  numpy==2.0.1
65
- # via
66
- # -r requirements.in
67
- # pandas
68
  openpyxl==3.1.5
69
  # via -r requirements.in
70
  packaging==25.0
@@ -105,13 +103,8 @@ setuptools==80.9.0
105
  # via dash
106
  six==1.17.0
107
  # via python-dateutil
108
- tomli==2.3.0
109
- # via
110
- # autoflake
111
- # black
112
  typing-extensions==4.15.0
113
  # via
114
- # black
115
  # dash
116
  # pydantic
117
  # pydantic-core
@@ -122,6 +115,8 @@ tzdata==2025.2
122
  # via pandas
123
  urllib3==2.5.0
124
  # via requests
 
 
125
  vizro==0.1.47
126
  # via -r requirements.in
127
  werkzeug==3.1.3
 
43
  idna==3.11
44
  # via requests
45
  importlib-metadata==8.7.0
46
+ # via dash
 
 
47
  itsdangerous==2.2.0
48
  # via flask
49
+ jellyfish==1.2.1
50
+ # via us
51
  jinja2==3.1.6
52
  # via flask
53
  markupsafe==3.0.3
 
62
  nest-asyncio==1.6.0
63
  # via dash
64
  numpy==2.0.1
65
+ # via pandas
 
 
66
  openpyxl==3.1.5
67
  # via -r requirements.in
68
  packaging==25.0
 
103
  # via dash
104
  six==1.17.0
105
  # via python-dateutil
 
 
 
 
106
  typing-extensions==4.15.0
107
  # via
 
108
  # dash
109
  # pydantic
110
  # pydantic-core
 
115
  # via pandas
116
  urllib3==2.5.0
117
  # via requests
118
+ us==3.2.0
119
+ # via -r requirements.in
120
  vizro==0.1.47
121
  # via -r requirements.in
122
  werkzeug==3.1.3
tables.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AG Grid table configurations for the Superstore BI dashboard."""
2
+
3
+ from dash_ag_grid import AgGrid
4
+ from data_processing import make_customer_sales_pareto_df
5
+ from vizro.models.types import capture
6
+ from vizro.tables import dash_ag_grid
7
+
8
+ # TODO: check and refactor
9
+ CELL_STYLE_PRODUCT = {
10
+ "styleConditions": [
11
+ {
12
+ "condition": "params.value < -0.5",
13
+ "style": {"backgroundColor": "#e33b3b"},
14
+ },
15
+ {
16
+ "condition": "params.value >= -0.5 && params.value <= 0",
17
+ "style": {"backgroundColor": "#f19791"},
18
+ },
19
+ {
20
+ "condition": "params.value > 0 && params.value <= 0.30",
21
+ "style": {"backgroundColor": "#728aff"},
22
+ },
23
+ {
24
+ "condition": "params.value > 0.30",
25
+ "style": {"backgroundColor": "#2251ff"},
26
+ },
27
+ ]
28
+ }
29
+
30
+
31
+ COLUMN_DEFS_PRODUCT = [
32
+ {"field": "Sub-Category", "cellDataType": "text", "headerName": "Sub-Category", "flex": 3},
33
+ {
34
+ "field": "Profit",
35
+ "cellDataType": "number",
36
+ "flex": 2,
37
+ "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
38
+ },
39
+ {
40
+ "field": "Sales",
41
+ "cellDataType": "number",
42
+ "flex": 2,
43
+ "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
44
+ },
45
+ {
46
+ "field": "Profit Margin",
47
+ "flex": 2,
48
+ "cellDataType": "number",
49
+ "valueFormatter": {"function": "d3.format('.0%')(params.value)"},
50
+ "cellStyle": CELL_STYLE_PRODUCT,
51
+ },
52
+ ]
53
+
54
+
55
+ @capture("ag_grid")
56
+ def orders_ag_grid(data_frame):
57
+ """Create custom AG Grid table for orders with conditional formatting.
58
+
59
+ Args:
60
+ data_frame: Source dataframe containing order data.
61
+
62
+ Returns:
63
+ AgGrid: Configured AG Grid component with custom column definitions and styling.
64
+ """
65
+ data_frame["Profit Ratio"] = (data_frame["Profit"] / data_frame["Sales"]).round(3)
66
+ column_defs_orders = [
67
+ {"headerName": "Order ID", "field": "Order ID", "minWidth": 150},
68
+ {"headerName": "Status", "field": "Order Status", "minWidth": 150, "cellRenderer": "statusCellRenderer"},
69
+ {
70
+ "headerName": "Segment",
71
+ "field": "Segment",
72
+ "minWidth": 140,
73
+ },
74
+ {"headerName": "Customer", "field": "Customer Name", "minWidth": 170},
75
+ {"headerName": "State", "field": "State", "minWidth": 150},
76
+ {"headerName": "City", "field": "City", "minWidth": 150},
77
+ {"headerName": "Category", "field": "Category", "minWidth": 150},
78
+ {"headerName": "Sub-Category", "field": "Sub-Category", "minWidth": 150},
79
+ {"headerName": "Sales", "field": "Sales", "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"}},
80
+ {
81
+ "headerName": "Profit",
82
+ "field": "Profit",
83
+ "valueFormatter": {"function": "d3.format('$,.2f')(params.value)"},
84
+ },
85
+ {
86
+ "headerName": "Profit Ratio",
87
+ "field": "Profit Ratio",
88
+ "minWidth": 140,
89
+ "valueFormatter": {"function": "d3.format('.1%')(params.value)"},
90
+ "cellStyle": {
91
+ "styleConditions": [
92
+ {
93
+ "condition": "Number(params.value) < -0.5",
94
+ "style": {
95
+ "backgroundColor": "#e33b3b",
96
+ "color": "white",
97
+ "borderRadius": "18px",
98
+ "padding": "4px",
99
+ "fontWeight": "600",
100
+ "justifyContent": "center",
101
+ "alignItems": "center",
102
+ "display": "flex",
103
+ "marginTop": "8px",
104
+ "height": "30px",
105
+ },
106
+ },
107
+ {
108
+ "condition": "Number(params.value) >= -0.5 && Number(params.value) < 0",
109
+ "style": {
110
+ "backgroundColor": "#f19791",
111
+ "color": "white",
112
+ "borderRadius": "18px",
113
+ "padding": "4px",
114
+ "fontWeight": "600",
115
+ "justifyContent": "center",
116
+ "alignItems": "center",
117
+ "display": "flex",
118
+ "marginTop": "8px",
119
+ "height": "30px",
120
+ },
121
+ },
122
+ {
123
+ "condition": "Number(params.value) >= 0 && Number(params.value) < 0.30",
124
+ "style": {
125
+ "backgroundColor": "#728aff",
126
+ "color": "white",
127
+ "borderRadius": "18px",
128
+ "padding": "4px",
129
+ "fontWeight": "600",
130
+ "justifyContent": "center",
131
+ "alignItems": "center",
132
+ "display": "flex",
133
+ "marginTop": "8px",
134
+ "height": "30px",
135
+ },
136
+ },
137
+ {
138
+ "condition": "Number(params.value) >= 0.30",
139
+ "style": {
140
+ "backgroundColor": "#2251ff",
141
+ "color": "white",
142
+ "borderRadius": "18px",
143
+ "padding": "4px",
144
+ "fontWeight": "600",
145
+ "justifyContent": "center",
146
+ "alignItems": "center",
147
+ "display": "flex",
148
+ "marginTop": "8px",
149
+ "height": "30px",
150
+ },
151
+ },
152
+ ]
153
+ },
154
+ },
155
+ ]
156
+
157
+ aggrid = AgGrid(
158
+ columnDefs=column_defs_orders,
159
+ defaultColDef={"resizable": True, "sortable": True, "filter": True, "minWidth": 30, "flex": 1},
160
+ style={"height": "800px", "width": "100%"},
161
+ rowData=data_frame.to_dict("records"),
162
+ dashGridOptions={
163
+ "rowHeight": 55,
164
+ "animateRows": True,
165
+ "suppressMovableColumns": True,
166
+ "pagination": True,
167
+ "paginationPageSize": 20,
168
+ },
169
+ dangerously_allow_code=True,
170
+ )
171
+ return aggrid
172
+
173
+
174
+ @capture("ag_grid")
175
+ def customers_ag_grid(data_frame):
176
+ data_frame = make_customer_sales_pareto_df(data_frame)
177
+ # Convert back to fraction since percentage is handled by cellDataType=percent.
178
+ data_frame["Cumulative %"] = data_frame["Cumulative % of Sales"] / 100
179
+
180
+ return dash_ag_grid(
181
+ data_frame,
182
+ columnDefs=[
183
+ {"field": "Rank", "width": 50},
184
+ {"field": "Customer Name"},
185
+ {"field": "Sales", "cellDataType": "dollar"},
186
+ {"field": "Cumulative %", "cellDataType": "percent", "width": 130},
187
+ ],
188
+ )()