Skip to content

Commit bcb1ae9

Browse files
committed
CDD-3295: Add chart labels, legends and chart settings
1 parent 2bd55d3 commit bcb1ae9

5 files changed

Lines changed: 166 additions & 3 deletions

File tree

metrics/api/serializers/charts/dual_category_charts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def to_models(self, request: Request) -> DualCategoryChartRequestParams:
116116
**static_fields,
117117
secondary_category: segment["secondary_field_value"],
118118
"chart_type": self.data["chart_type"],
119+
"label": segment["label"],
119120
}
120121
for segment in segments
121122
]
@@ -132,6 +133,7 @@ def to_models(self, request: Request) -> DualCategoryChartRequestParams:
132133
x_axis: primary_field_value,
133134
secondary_category: segment["secondary_field_value"],
134135
"chart_type": self.data["chart_type"],
136+
"label": segment["label"],
135137
}
136138
for segment in segments
137139
]
@@ -154,4 +156,5 @@ def to_models(self, request: Request) -> DualCategoryChartRequestParams:
154156
or DEFAULT_Y_AXIS_MINIMUM_VAlUE,
155157
y_axis_maximum_value=self.data["y_axis_maximum_value"],
156158
request=request,
159+
legend_title=self.data.get("legend_title", ""),
157160
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from metrics.domain.charts.chart_settings.single_category import (
2+
SingleCategoryChartSettings,
3+
)
4+
from metrics.domain.models.plots import ChartGenerationPayload, PlotGenerationData
5+
6+
7+
class DualCategoryChartSettings(SingleCategoryChartSettings):
8+
def __init__(self, *, chart_generation_payload: ChartGenerationPayload):
9+
super().__init__(chart_generation_payload=chart_generation_payload)
10+
self.plots_data: list[PlotGenerationData] = chart_generation_payload.plots
11+
12+
def get_stacked_bar_chart_config(self) -> dict:
13+
"""
14+
Builds the configuration for the stacked bar chart.
15+
16+
Returns:
17+
The configuration for the stacked bar chart.
18+
"""
19+
chart_config = self._get_base_chart_config()
20+
21+
chart_config["barmode"] = "stack"
22+
23+
return {**chart_config, **self._get_legend_config()}
24+
25+
def _get_legend_config(self) -> dict:
26+
"""
27+
Builds the configuration for the legend.
28+
29+
Returns:
30+
The configuration for the legend.
31+
"""
32+
legend_config = {
33+
"font": self._get_tick_font_config(),
34+
"orientation": "h",
35+
"y": 1.0,
36+
"x": 0.5,
37+
"xanchor": "center",
38+
"yanchor": "bottom",
39+
"entrywidth": 80,
40+
}
41+
42+
if legend_title := self._chart_generation_payload.legend_title:
43+
legend_config["title"] = {
44+
"text": f"<b>{legend_title}</b>",
45+
"side": "top",
46+
}
47+
48+
return {
49+
"legend": legend_config,
50+
}

metrics/domain/charts/stacked_bar/generation.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import plotly.graph_objects as go
44

5+
from metrics.domain.charts.chart_settings.dual_category import DualCategoryChartSettings
56
from metrics.domain.models.plots import ChartGenerationPayload
67

78

@@ -26,19 +27,23 @@ def generate_stacked_bar(
2627

2728
secondary_category = chart_generation_payload.secondary_category
2829
for plot in chart_generation_payload.plots:
29-
group = plot.parameters.model_dump()[secondary_category]
30+
group = getattr(plot.parameters, secondary_category)
3031
grouped[group]["x_axis_values"].extend(plot.x_axis_values)
3132
grouped[group]["y_axis_values"].extend(plot.y_axis_values)
33+
grouped[group]["label"] = plot.parameters.label
3234

3335
for label, data in grouped.items():
3436
figure.add_trace(
3537
go.Bar(
3638
x=data["x_axis_values"],
3739
y=data["y_axis_values"],
38-
name=label,
40+
name=data["label"] or label,
3941
)
4042
)
4143

42-
figure.update_layout({"xaxis": {"type": "category"}}, barmode="stack")
44+
settings = DualCategoryChartSettings(
45+
chart_generation_payload=chart_generation_payload,
46+
)
47+
figure.update_layout(**settings.get_stacked_bar_chart_config())
4348

4449
return figure

metrics/domain/models/charts/dual_category_charts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ class DualCategoryChartRequestParams(BaseChartRequestParams):
2222
primary_field_values: list[str]
2323
static_fields: StaticFields
2424
plots: list[PlotParameters]
25+
legend_title: str | None = ""
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
3+
from metrics.domain.charts.chart_settings.dual_category import DualCategoryChartSettings
4+
from metrics.domain.models import ChartGenerationPayload, PlotGenerationData
5+
from tests.conftest import fake_plot_data
6+
7+
8+
@pytest.fixture()
9+
def fake_dual_category_chart_settings(
10+
fake_plot_data: PlotGenerationData,
11+
) -> DualCategoryChartSettings:
12+
payload = ChartGenerationPayload(
13+
chart_width=640,
14+
chart_height=400,
15+
plots=[fake_plot_data],
16+
x_axis_title="Date",
17+
y_axis_title="Cases",
18+
)
19+
return DualCategoryChartSettings(chart_generation_payload=payload)
20+
21+
22+
class TestDualCategoryChartSettings:
23+
def test_get_legend_config(
24+
self, fake_dual_category_chart_settings: DualCategoryChartSettings
25+
):
26+
"""
27+
Given an instance of `DualCategoryChartSettings` without a legend title
28+
When `_get_legend_config()` is called
29+
Then the legend configuration is returned without a title
30+
"""
31+
# Given
32+
chart_settings = fake_dual_category_chart_settings
33+
34+
# When
35+
legend_config = chart_settings._get_legend_config()
36+
37+
# Then
38+
assert legend_config == {
39+
"legend": {
40+
"font": chart_settings._get_tick_font_config(),
41+
"orientation": "h",
42+
"y": 1.0,
43+
"x": 0.5,
44+
"xanchor": "center",
45+
"yanchor": "bottom",
46+
"entrywidth": 80,
47+
},
48+
}
49+
50+
def test_get_legend_config_includes_legend_title_when_provided(
51+
self, fake_dual_category_chart_settings: DualCategoryChartSettings
52+
):
53+
"""
54+
Given an instance of `DualCategoryChartSettings` with a legend title
55+
When `_get_legend_config()` is called
56+
Then the legend configuration includes a formatted title
57+
"""
58+
# Given
59+
chart_settings = fake_dual_category_chart_settings
60+
legend_title = "Age group"
61+
chart_settings._chart_generation_payload.legend_title = legend_title
62+
63+
# When
64+
legend_config = chart_settings._get_legend_config()
65+
66+
# Then
67+
assert legend_config == {
68+
"legend": {
69+
"font": chart_settings._get_tick_font_config(),
70+
"orientation": "h",
71+
"y": 1.0,
72+
"x": 0.5,
73+
"xanchor": "center",
74+
"yanchor": "bottom",
75+
"entrywidth": 80,
76+
"title": {
77+
"text": f"<b>{legend_title}</b>",
78+
"side": "top",
79+
},
80+
},
81+
}
82+
83+
def test_get_stacked_bar_chart_config(
84+
self, fake_dual_category_chart_settings: DualCategoryChartSettings
85+
):
86+
"""
87+
Given an instance of `DualCategoryChartSettings`
88+
When `get_stacked_bar_chart_config()` is called
89+
Then stacked bar layout and legend settings are merged
90+
"""
91+
# Given
92+
chart_settings = fake_dual_category_chart_settings
93+
94+
# When
95+
stacked_bar_config = chart_settings.get_stacked_bar_chart_config()
96+
97+
expected_config = {
98+
**chart_settings._get_base_chart_config(),
99+
**chart_settings._get_legend_config(),
100+
"barmode": "stack",
101+
}
102+
103+
# Then
104+
assert stacked_bar_config == expected_config

0 commit comments

Comments
 (0)