antonymilne Claude commited on
Commit
22e2ea0
·
1 Parent(s): d267326

Refactor KPI cards and chart functions with shared constants

Browse files

- Add THIS_YEAR, LAST_YEAR, COLUMN_TO_METRIC, COLUMN_TO_AGGFUNC constants to data_processing.py
- Update create_kpi_card_with_action to use new constants
- Rename chart functions to use "overview_by_" prefix for consistency
- Simplify overview_by_month to use px.line instead of go.Figure with add_trace
- Update all function references in app.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (4) hide show
  1. app.py +34 -31
  2. charts.py +24 -59
  3. data_processing.py +6 -1
  4. tables.py +1 -1
app.py CHANGED
@@ -6,20 +6,28 @@ import vizro.actions as va
6
  from vizro.figures import kpi_card_reference
7
  from vizro.tables import dash_ag_grid
8
 
9
- from data_processing import make_superstore_profit_df, make_superstore_pareto_df, make_superstore_df
 
 
 
 
 
 
 
 
10
  from charts import (
11
  bar_chart_by_category,
12
  create_map_bubble_new,
13
- create_line_chart_per_month,
14
- create_bar_current_vs_previous_segment,
15
- create_bar_current_vs_previous_category,
16
  pareto_customers_chart,
17
  scatter_with_quadrants,
18
- pie_chart_by_order_status,
19
  bar_chart_top_n,
20
- create_lollipop_chart_by_region,
21
  )
22
- from tables import COLUMN_DEFS_PRODUCT, COLUMN_DEFS_CUSTOMERS, custom_orders_aggrid
23
 
24
 
25
  ###############################################################################
@@ -37,21 +45,19 @@ def create_filled_container(**kwargs):
37
  ###############################################################################
38
  # Overview page
39
  ###############################################################################
40
- def create_kpi_card_with_action(data_frame, title, icon, column=None, aggfunc="sum", prefix=""):
41
  """Create a KPI card figure that updates the metric control when clicked."""
42
- column = column or title
43
-
44
  # Pivot and aggregate to produce dataframe with column for each year for specified metric.
45
- data_frame = data_frame.pivot_table(values=column, columns="Year", aggfunc=aggfunc)
46
 
47
  return vm.Figure(
48
  figure=kpi_card_reference(
49
  data_frame=data_frame,
50
- value_column=data_frame.columns.max(), # This year
51
- reference_column=data_frame.columns.min(), # Last year
52
  value_format=f"{prefix}{{value:,.0f}}",
53
  reference_format=f"{{delta_relative:+.1%}} vs. last year ({prefix}{{reference:,.0f}})",
54
- title=title,
55
  icon=icon,
56
  ),
57
  actions=va.set_control(control="metric", value=column),
@@ -71,10 +77,10 @@ kpi_card_container = vm.Container(
71
  title="💡 Click on a KPI card to update the charts below.",
72
  layout=vm.Grid(grid=[[0, 1, 2, 3]]),
73
  components=[
74
- create_kpi_card_with_action(df, title="Sales", icon="Bar Chart", prefix="$"),
75
- create_kpi_card_with_action(df, title="Profit", icon="Money Bag", prefix="$"),
76
- create_kpi_card_with_action(df, title="Orders", icon="Orders", column="Order ID", aggfunc="nunique"),
77
- create_kpi_card_with_action(df, title="Customers", icon="Group", column="Customer ID", aggfunc="nunique"),
78
  ],
79
  )
80
 
@@ -84,21 +90,21 @@ overview_page = vm.Page(
84
  layout=vm.Grid(grid=[[0, 0, 0]] * 3 + [[1, 1, 2]] * 5 + [[3, 4, 5]] * 5),
85
  components=[
86
  kpi_card_container,
87
- create_filled_container(components=[vm.Graph(id="line_month_chart", figure=create_line_chart_per_month(df))]),
88
  create_chart_container_with_nav(
89
- graph=vm.Graph(id="order_status_chart", figure=pie_chart_by_order_status(df)),
90
  nav_href="/orders",
91
  ),
92
  create_chart_container_with_nav(
93
- graph=vm.Graph(id="region_bar_chart", figure=create_lollipop_chart_by_region(df)),
94
  nav_href="/regions",
95
  ),
96
  create_chart_container_with_nav(
97
- graph=vm.Graph(id="segment_bar_chart", figure=create_bar_current_vs_previous_segment(df)),
98
  nav_href="/customers",
99
  ),
100
  create_chart_container_with_nav(
101
- graph=vm.Graph(id="category_bar_chart", figure=create_bar_current_vs_previous_category(df)),
102
  nav_href="/products",
103
  ),
104
  ],
@@ -109,11 +115,10 @@ overview_page = vm.Page(
109
  targets=[
110
  "region_bar_chart.value_col",
111
  "category_bar_chart.value_col",
112
- "order_status_chart.value_col",
113
- "line_month_chart.value_col",
114
  "segment_bar_chart.value_col",
115
  ],
116
- show_in_url=True, # TODO: think about where this needed
117
  visible=False,
118
  )
119
  ],
@@ -157,7 +162,7 @@ regions_page = vm.Page(
157
  vm.Filter(column="Segment", selector=vm.Checklist(title="Choose segment")),
158
  vm.Filter(column="Category", selector=vm.Checklist(title="Choose category")),
159
  vm.Parameter(
160
- targets=["usa_map.value_col", "top_n_bars.x"],
161
  selector=vm.RadioItems(options=["Sales", "Profit"], title="Choose metric"),
162
  ),
163
  ],
@@ -265,7 +270,7 @@ orders_page = vm.Page(
265
  title="Orders",
266
  layout=vm.Flex(),
267
  components=[
268
- vm.AgGrid(figure=custom_orders_aggrid(df)),
269
  vm.Button(text="Export data", icon="download", actions=va.export_data(file_format="xlsx")),
270
  ],
271
  )
@@ -287,9 +292,7 @@ navigation = vm.Navigation(
287
  ),
288
  )
289
 
290
- dashboard = vm.Dashboard(
291
- title="Superstore dashboard", pages=list(pages.values()), navigation=navigation, theme="vizro_light"
292
- )
293
 
294
  app = Vizro().build(dashboard)
295
 
 
6
  from vizro.figures import kpi_card_reference
7
  from vizro.tables import dash_ag_grid
8
 
9
+ from data_processing import (
10
+ make_superstore_profit_df,
11
+ make_superstore_pareto_df,
12
+ make_superstore_df,
13
+ COLUMN_TO_METRIC,
14
+ COLUMN_TO_AGGFUNC,
15
+ THIS_YEAR,
16
+ LAST_YEAR,
17
+ )
18
  from charts import (
19
  bar_chart_by_category,
20
  create_map_bubble_new,
21
+ overview_by_month,
22
+ overview_by_customer_segment,
23
+ overview_by_product_category,
24
  pareto_customers_chart,
25
  scatter_with_quadrants,
26
+ overview_by_order_status,
27
  bar_chart_top_n,
28
+ overview_by_region,
29
  )
30
+ from tables import COLUMN_DEFS_PRODUCT, COLUMN_DEFS_CUSTOMERS, orders_ag_grid
31
 
32
 
33
  ###############################################################################
 
45
  ###############################################################################
46
  # Overview page
47
  ###############################################################################
48
+ def create_kpi_card_with_action(data_frame, column, icon, prefix=""):
49
  """Create a KPI card figure that updates the metric control when clicked."""
 
 
50
  # Pivot and aggregate to produce dataframe with column for each year for specified metric.
51
+ data_frame = data_frame.pivot_table(values=column, columns="Year", aggfunc=COLUMN_TO_AGGFUNC[column])
52
 
53
  return vm.Figure(
54
  figure=kpi_card_reference(
55
  data_frame=data_frame,
56
+ value_column=THIS_YEAR, # This year
57
+ reference_column=LAST_YEAR, # Last year
58
  value_format=f"{prefix}{{value:,.0f}}",
59
  reference_format=f"{{delta_relative:+.1%}} vs. last year ({prefix}{{reference:,.0f}})",
60
+ title=COLUMN_TO_METRIC[column],
61
  icon=icon,
62
  ),
63
  actions=va.set_control(control="metric", value=column),
 
77
  title="💡 Click on a KPI card to update the charts below.",
78
  layout=vm.Grid(grid=[[0, 1, 2, 3]]),
79
  components=[
80
+ create_kpi_card_with_action(df, column="Sales", icon="Bar Chart", prefix="$"),
81
+ create_kpi_card_with_action(df, column="Profit", icon="Money Bag", prefix="$"),
82
+ create_kpi_card_with_action(df, column="Order ID", icon="Orders"),
83
+ create_kpi_card_with_action(df, column="Customer ID", icon="Group"),
84
  ],
85
  )
86
 
 
90
  layout=vm.Grid(grid=[[0, 0, 0]] * 3 + [[1, 1, 2]] * 5 + [[3, 4, 5]] * 5),
91
  components=[
92
  kpi_card_container,
93
+ create_filled_container(components=[vm.Graph(id="month_line_chart", figure=overview_by_month(df))]),
94
  create_chart_container_with_nav(
95
+ graph=vm.Graph(id="order_status_pie_chart", figure=overview_by_order_status(df)),
96
  nav_href="/orders",
97
  ),
98
  create_chart_container_with_nav(
99
+ graph=vm.Graph(id="region_bar_chart", figure=overview_by_region(df)),
100
  nav_href="/regions",
101
  ),
102
  create_chart_container_with_nav(
103
+ graph=vm.Graph(id="segment_bar_chart", figure=overview_by_customer_segment(df)),
104
  nav_href="/customers",
105
  ),
106
  create_chart_container_with_nav(
107
+ graph=vm.Graph(id="category_bar_chart", figure=overview_by_product_category(df)),
108
  nav_href="/products",
109
  ),
110
  ],
 
115
  targets=[
116
  "region_bar_chart.value_col",
117
  "category_bar_chart.value_col",
118
+ "order_status_pie_chart.value_col",
119
+ "month_line_chart.column",
120
  "segment_bar_chart.value_col",
121
  ],
 
122
  visible=False,
123
  )
124
  ],
 
162
  vm.Filter(column="Segment", selector=vm.Checklist(title="Choose segment")),
163
  vm.Filter(column="Category", selector=vm.Checklist(title="Choose category")),
164
  vm.Parameter(
165
+ targets=["usa_map.column", "top_n_bars.x"],
166
  selector=vm.RadioItems(options=["Sales", "Profit"], title="Choose metric"),
167
  ),
168
  ],
 
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
  )
 
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
 
charts.py CHANGED
@@ -3,6 +3,7 @@
3
  import pandas as pd
4
  import plotly.graph_objects as go
5
  import vizro.plotly.express as px
 
6
  from vizro.models.types import capture
7
 
8
  CURRENT_YEAR = 2017
@@ -111,7 +112,7 @@ def create_map_bubble_new(data_frame, custom_data, value_col="Sales"):
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":
@@ -169,7 +170,7 @@ def create_lollipop_chart_by_region(data_frame, value_col="Sales"):
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
@@ -229,7 +230,7 @@ def create_bar_current_vs_previous_segment(data_frame, value_col="Sales"):
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
 
@@ -287,73 +288,37 @@ def create_bar_current_vs_previous_category(data_frame, value_col="Sales"):
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 = (
 
3
  import pandas as pd
4
  import plotly.graph_objects as go
5
  import vizro.plotly.express as px
6
+ from data_processing import COLUMN_TO_METRIC, COLUMN_TO_AGGFUNC, LAST_YEAR, THIS_YEAR
7
  from vizro.models.types import capture
8
 
9
  CURRENT_YEAR = 2017
 
112
 
113
 
114
  @capture("graph")
115
+ def overview_by_region(data_frame, value_col="Sales"):
116
  """Create a lollipop chart showing aggregated metrics by region using Plotly."""
117
  # --- Aggregate based on chosen metric ---
118
  if value_col == "Order ID":
 
170
 
171
 
172
  @capture("graph")
173
+ def overview_by_customer_segment(data_frame, value_col="Sales"):
174
  """Custom bar chart made with Plotly."""
175
  data_frame["Order Date"] = pd.to_datetime(data_frame["Order Date"])
176
  data_frame["Year"] = data_frame["Order Date"].dt.year
 
230
 
231
 
232
  @capture("graph")
233
+ def overview_by_product_category(data_frame, value_col="Sales"):
234
  """Bar chart comparing current year vs previous year by category."""
235
  data_frame["Year"] = data_frame["Order Date"].dt.year
236
 
 
288
  return fig
289
 
290
 
291
+ import calendar
 
 
 
 
 
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
+ @capture("graph")
295
+ def overview_by_month(data_frame, column="Sales"):
296
+ grouped_df = data_frame.groupby(["Year", "Month"], as_index=False).agg({column: COLUMN_TO_AGGFUNC[column]})
297
+ # Relabel for plotting in human-readable way.
298
+ grouped_df["Month"] = grouped_df["Month"].map({i: calendar.month_abbr[i] for i in range(1, 13)})
299
+ grouped_df["Year"] = grouped_df["Year"].map({THIS_YEAR: "This year", LAST_YEAR: "Last year"})
300
 
301
+ fig = px.line(
302
+ grouped_df,
303
+ x="Month",
304
+ color="Year",
305
+ y=column,
306
+ markers=True,
307
+ title=f"{COLUMN_TO_METRIC[column]} | By Month",
308
+ color_discrete_map={"This year": PRIMARY_COLOR, "Last year": SECONDARY_COLOR},
 
 
 
 
 
 
 
 
 
 
309
  )
310
+ fig.data[1].update(line_width=2, fill="tozeroy")
311
  fig.update_layout(
312
+ xaxis={"showgrid": False, "title": None, "range": [-0.1, 11.1]},
 
 
 
 
 
 
 
313
  yaxis_title=None,
314
+ title=f"{column} | By Month",
315
+ legend={"yanchor": "top", "y": 1.2, "xanchor": "right", "x": 1, "title": None},
316
  )
 
317
  return fig
318
 
319
 
320
  @capture("graph")
321
+ def overview_by_order_status(data_frame, value_col="Sales"):
322
  """Pie chart showing distribution by Order Status."""
323
  if value_col == "Order ID":
324
  status_metric = (
data_processing.py CHANGED
@@ -5,6 +5,11 @@ import random
5
  import pandas as pd
6
  import us
7
 
 
 
 
 
 
8
 
9
  def make_superstore_df():
10
  """Load and preprocess the Superstore dataset.
@@ -21,7 +26,7 @@ def make_superstore_df():
21
  # Filter dataframe for only the latest 2 years and add a month column.
22
  data_frame["Year"] = data_frame["Order Date"].dt.year
23
  data_frame["Month"] = data_frame["Order Date"].dt.month
24
- data_frame = data_frame[data_frame["Year"].isin(sorted(data_frame["Year"].unique())[-2:])]
25
 
26
  # Create order status - randomly assign one of three status values
27
  data_frame["Order Status"] = random.choices(["Delivered", "In Transit", "Processing"], k=len(data_frame))
 
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.
 
26
  # Filter dataframe for only the latest 2 years and add a month column.
27
  data_frame["Year"] = data_frame["Order Date"].dt.year
28
  data_frame["Month"] = data_frame["Order Date"].dt.month
29
+ data_frame = data_frame[data_frame["Year"].isin([THIS_YEAR, LAST_YEAR])]
30
 
31
  # Create order status - randomly assign one of three status values
32
  data_frame["Order Status"] = random.choices(["Delivered", "In Transit", "Processing"], k=len(data_frame))
tables.py CHANGED
@@ -104,7 +104,7 @@ COLUMN_DEFS_CUSTOMERS = [
104
 
105
 
106
  @capture("ag_grid")
107
- def custom_orders_aggrid(data_frame):
108
  """Create custom AG Grid table for orders with conditional formatting.
109
 
110
  Args:
 
104
 
105
 
106
  @capture("ag_grid")
107
+ def orders_ag_grid(data_frame):
108
  """Create custom AG Grid table for orders with conditional formatting.
109
 
110
  Args: