Skip to content

Commit 89d9497

Browse files
committed
CDD-3295: Add dual category chart.
1 parent 7f00724 commit 89d9497

31 files changed

Lines changed: 1441 additions & 91 deletions

File tree

metrics/api/serializers/charts/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
ChartPlotSerializer,
33
ChartPlotsListSerializer,
44
ChartsResponseSerializer,
5-
EncodedChartResponseSerializer,
65
EncodedChartsRequestSerializer,
76
ChartsSerializer,
87
)

metrics/api/serializers/charts/common.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515

1616
class BaseChartsSerializer(serializers.Serializer):
17+
"""Base serializer for chart request payloads, containing common fields across different chart types."""
18+
1719
file_format = serializers.ChoiceField(
1820
choices=FILE_FORMAT_CHOICES,
1921
help_text=help_texts.CHART_FILE_FORMAT_FIELD,
@@ -94,3 +96,21 @@ class BaseChartsSerializer(serializers.Serializer):
9496
allow_null=True,
9597
default="",
9698
)
99+
100+
101+
class ChartPreviewQueryParamsSerializer(serializers.Serializer):
102+
"""Serializer for query parameters when requesting a chart preview."""
103+
104+
preview = serializers.BooleanField(required=False)
105+
106+
107+
class EncodedChartResponseSerializer(serializers.Serializer):
108+
"""Serializer for the response of an encoded chart generation, containing the encoded chart and related metadata."""
109+
110+
last_updated = serializers.CharField(
111+
help_text=help_texts.ENCODED_CHARTS_LAST_UPDATED,
112+
allow_blank=True,
113+
)
114+
chart = serializers.CharField(help_text=help_texts.ENCODED_CHARTS_RESPONSE)
115+
alt_text = serializers.CharField(help_text=help_texts.CHARTS_ALT_TEXT)
116+
figure = serializers.DictField(help_text=help_texts.CHARTS_FIGURE_OUTPUT)

metrics/api/serializers/charts/dual_category_charts.py

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
DEFAULT_CHART_WIDTH,
1111
DEFAULT_X_AXIS,
1212
DEFAULT_Y_AXIS,
13+
ChartAxisFields,
1314
ChartTypes,
15+
DataSourceFileType,
1416
DEFAULT_Y_AXIS_MINIMUM_VAlUE,
17+
extract_metric_group_from_metric,
18+
)
19+
from metrics.domain.models.charts.dual_category_charts import (
20+
DualCategoryChartRequestParams,
1521
)
16-
from metrics.domain.models.charts import DualCategoryChartRequestParams
1722

1823

1924
class DualCategoryChartSegmentSerializer(serializers.Serializer):
@@ -33,59 +38,125 @@ class DualCategoryChartSegmentSerializer(serializers.Serializer):
3338
)
3439

3540

36-
class StaticFieldsSerializer(PlotSerializer):
37-
theme = serializers.CharField(
38-
required=True,
39-
allow_blank=True,
40-
allow_null=True,
41-
)
42-
sub_theme = serializers.CharField(
43-
required=True,
44-
allow_blank=True,
45-
allow_null=True,
46-
)
47-
48-
4941
class DualCategoryChartSerializer(BaseChartsSerializer):
5042
chart_type = serializers.ChoiceField(
5143
help_text=help_texts.CHART_TYPE_FIELD,
52-
choices=ChartTypes.selectable_choices(),
44+
choices=ChartTypes.dual_category_chart_options(),
5345
required=True,
5446
)
5547
primary_field_values = serializers.ListField(
5648
child=serializers.CharField(),
5749
help_text="List of primary field values for this segment",
58-
required=True,
59-
allow_empty=False,
50+
required=False,
51+
allow_empty=True,
6052
)
6153

6254
secondary_category = serializers.CharField(
6355
help_text="Secondary category field for the chart",
6456
required=True,
6557
)
6658

67-
static_fields = StaticFieldsSerializer()
59+
static_fields = PlotSerializer()
6860

6961
segments = serializers.ListField(
7062
child=DualCategoryChartSegmentSerializer(),
7163
help_text="Segments for the dual category chart",
7264
required=True,
7365
)
7466

67+
@classmethod
68+
def validate(cls, attrs: dict) -> dict:
69+
"""Validate primary_field_values based on the selected x-axis."""
70+
x_axis = attrs.get("x_axis") or DEFAULT_X_AXIS
71+
primary_field_values = attrs.get("primary_field_values") or []
72+
metric = attrs["static_fields"]["metric"]
73+
metric_group = extract_metric_group_from_metric(metric=metric)
74+
is_timeseries_data = DataSourceFileType[metric_group].is_timeseries
75+
76+
if is_timeseries_data:
77+
if primary_field_values:
78+
raise serializers.ValidationError(
79+
{
80+
"primary_field_values": (
81+
"This field should not be provided for timeseries data."
82+
)
83+
}
84+
)
85+
if x_axis != ChartAxisFields.date.name:
86+
raise serializers.ValidationError(
87+
{
88+
"x_axis": (
89+
"This field should be set to 'date' for timeseries data."
90+
)
91+
}
92+
)
93+
94+
elif not is_timeseries_data and not primary_field_values:
95+
raise serializers.ValidationError(
96+
{"primary_field_values": ("This field is required for headline data.")}
97+
)
98+
99+
return attrs
100+
75101
def to_models(self, request: Request) -> DualCategoryChartRequestParams:
76102
x_axis = self.data.get("x_axis") or DEFAULT_X_AXIS
77103
y_axis = self.data.get("y_axis") or DEFAULT_Y_AXIS
78104

79-
for plot in self.data["segments"]:
80-
plot["x_axis"] = x_axis
81-
plot["y_axis"] = y_axis
105+
primary_field_values = self.data.get("primary_field_values") or []
106+
secondary_category = self.data["secondary_category"]
107+
static_fields: dict[str, str | int] = self.validated_data.pop("static_fields")
108+
109+
if static_fields["date_to"]:
110+
static_fields["date_to"] = static_fields["date_to"].isoformat()
111+
112+
if static_fields["date_from"]:
113+
static_fields["date_from"] = static_fields["date_from"].isoformat()
114+
115+
groups_plots = []
116+
segments: list[dict] = self.data["segments"]
117+
118+
metric_group = extract_metric_group_from_metric(metric=static_fields["metric"])
119+
is_timeseries_data = DataSourceFileType[metric_group].is_timeseries
120+
121+
# If timeseries data
122+
if is_timeseries_data:
123+
plots = [
124+
{
125+
"x_axis": x_axis,
126+
"y_axis": y_axis,
127+
"line_colour": segment["colour"],
128+
**static_fields,
129+
secondary_category: segment["secondary_field_value"],
130+
"chart_type": self.data["chart_type"],
131+
"label": segment["label"],
132+
}
133+
for segment in segments
134+
]
135+
groups_plots.extend(plots)
136+
137+
else:
138+
for primary_field_value in primary_field_values:
139+
plots = [
140+
{
141+
"x_axis": x_axis,
142+
"y_axis": y_axis,
143+
"line_colour": segment["colour"],
144+
**static_fields,
145+
x_axis: primary_field_value,
146+
secondary_category: segment["secondary_field_value"],
147+
"chart_type": self.data["chart_type"],
148+
"label": segment["label"],
149+
}
150+
for segment in segments
151+
]
152+
groups_plots.extend(plots)
82153

83154
return DualCategoryChartRequestParams(
84155
chart_type=self.data["chart_type"],
85-
primary_field_values=self.data["primary_field_values"],
156+
primary_field_values=primary_field_values,
86157
secondary_category=self.data["secondary_category"],
87158
static_fields=self.data["static_fields"],
88-
segments=self.data["segments"],
159+
plots=groups_plots,
89160
file_format=self.data["file_format"],
90161
chart_height=self.data["chart_height"] or DEFAULT_CHART_HEIGHT,
91162
chart_width=self.data["chart_width"] or DEFAULT_CHART_WIDTH,
@@ -97,4 +168,5 @@ def to_models(self, request: Request) -> DualCategoryChartRequestParams:
97168
or DEFAULT_Y_AXIS_MINIMUM_VAlUE,
98169
y_axis_maximum_value=self.data["y_axis_maximum_value"],
99170
request=request,
171+
legend_title=self.data.get("legend_title", ""),
100172
)

metrics/api/serializers/charts/single_category_charts.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,3 @@ class EncodedChartsRequestSerializer(ChartsSerializer):
112112
help_text=help_texts.ENCODED_CHARTS_FILE_FORMAT_FIELD,
113113
default="svg",
114114
)
115-
116-
117-
class EncodedChartResponseSerializer(serializers.Serializer):
118-
last_updated = serializers.CharField(
119-
help_text=help_texts.ENCODED_CHARTS_LAST_UPDATED,
120-
allow_blank=True,
121-
)
122-
chart = serializers.CharField(help_text=help_texts.ENCODED_CHARTS_RESPONSE)
123-
alt_text = serializers.CharField(help_text=help_texts.CHARTS_ALT_TEXT)
124-
figure = serializers.DictField(help_text=help_texts.CHARTS_FIGURE_OUTPUT)

metrics/api/serializers/charts/subplot_charts.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,3 @@ def to_models(self, request: Request) -> SubplotChartRequestParameters:
213213
subplots=self.validated_data["subplots"],
214214
request=request,
215215
)
216-
217-
218-
class ChartPreviewQueryParamsSerializer(serializers.Serializer):
219-
preview = serializers.BooleanField(required=False)

0 commit comments

Comments
 (0)