Skip to content

Commit

Permalink
Merge pull request #2050 from UKHSA-Internal/task/cdd-2371-manual-y-a…
Browse files Browse the repository at this point in the history
…xis-range-landing-page

CDD-2371: Add support for manually setting min and max y-axis range
  • Loading branch information
phill-stanley authored Jan 17, 2025
2 parents d6ca0c1 + f605d47 commit 2088894
Show file tree
Hide file tree
Showing 14 changed files with 2,280 additions and 9 deletions.
9 changes: 9 additions & 0 deletions cms/dynamic_content/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ class SimplifiedChartWithLink(blocks.StructBlock):
default="",
help_text=help_texts.CHART_Y_AXIS_TITLE,
)
y_axis_minimum_value = blocks.DecimalBlock(
required=False,
default=0,
help_text=help_texts.CHART_Y_AXIS_MINIMUM_VALUE,
)
y_axis_maximum_value = blocks.DecimalBlock(
required=False,
help_text=help_texts.CHART_Y_AXIS_MAXIMUM_VALUE,
)
chart = SimplifiedChartComponent(
help_text=help_texts.CHART_BLOCK_FIELD,
required=True,
Expand Down
12 changes: 12 additions & 0 deletions cms/dynamic_content/help_texts.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,18 @@
If nothing is provided, `metric value` will be used by default.
"""

CHART_Y_AXIS_MINIMUM_VALUE: str = """
This field allows you to set the first value in the chart's y-axis range. Please
note that a value provided here, which is higher than the lowest value in the data will
be overridden and the value from the dataset will be used.
"""

CHART_Y_AXIS_MAXIMUM_VALUE: str = """
This field allows you to set the last value in the chart's y-axis range. Please
note that a value provided here, which is lower than the highest value in the data will
be overridden and the value from the dataset will be used.
"""

REQUIRED_CHART_Y_AXIS: str = """
A required choice of what to display along the y-axis of the chart.
"""
Expand Down
1,296 changes: 1,296 additions & 0 deletions cms/home/migrations/0016_add_chart_y_axis_min_and_max_values.py

Large diffs are not rendered by default.

640 changes: 640 additions & 0 deletions cms/topic/migrations/0015_add_chart_y_axis_min_and_max_values.py

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions metrics/api/serializers/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
DEFAULT_Y_AXIS,
ChartAxisFields,
ChartTypes,
DEFAULT_Y_AXIS_MINIMUM_VAlUE,
)
from metrics.domain.models import ChartRequestParams, PlotParameters

Expand Down Expand Up @@ -116,6 +117,22 @@ class ChartsSerializer(serializers.Serializer):
default="",
help_text=help_texts.CHART_Y_AXIS_TITLE,
)
y_axis_minimum_value = serializers.DecimalField(
required=False,
allow_null=True,
help_text=help_texts.CHART_Y_AXIS_MINIMUM_VALUE,
default=None,
max_digits=10,
decimal_places=1,
)
y_axis_maximum_value = serializers.DecimalField(
required=False,
allow_null=True,
help_text=help_texts.CHART_Y_AXIS_MAXIMUM_VALUE,
default=None,
max_digits=10,
decimal_places=1,
)

plots = ChartPlotsListSerializer()

Expand All @@ -136,6 +153,9 @@ def to_models(self) -> ChartRequestParams:
y_axis=y_axis,
x_axis_title=self.data.get("x_axis_title", ""),
y_axis_title=self.data.get("y_axis_title", ""),
y_axis_minimum_value=self.data["y_axis_minimum_value"]
or DEFAULT_Y_AXIS_MINIMUM_VAlUE,
y_axis_maximum_value=self.data["y_axis_maximum_value"],
)


Expand Down
7 changes: 7 additions & 0 deletions metrics/api/serializers/help_texts.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@
CHART_Y_AXIS_TITLE: str = """
An optional title to display along the Y axis of the chart
"""
CHART_Y_AXIS_MINIMUM_VALUE: str = """
The value used to start the chart's y_axis Eg: 0 will scale the chart to have a 0 as its lowest value.
"""
CHART_Y_AXIS_MAXIMUM_VALUE: str = """
The value used as the chart's highest y_axis value Eg: providing a higher value than appears in the data will
rescale the chart to that value
"""
ENCODED_CHARTS_RESPONSE: str = """
The specified chart in the requested format as a URI encoded string (default format = svg)
"""
Expand Down
81 changes: 73 additions & 8 deletions metrics/domain/charts/chart_settings.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import datetime
import logging
from decimal import Decimal

from metrics.domain.charts import colour_scheme
from metrics.domain.charts.type_hints import DICT_OF_STR_ONLY
from metrics.domain.charts.utils import return_formatted_max_y_axis_value
from metrics.domain.charts.utils import (
return_formatted_max_y_axis_value,
return_formatted_min_y_axis_value,
)
from metrics.domain.common.utils import DEFAULT_CHART_WIDTH, get_last_day_of_month
from metrics.domain.models import PlotGenerationData
from metrics.domain.models.plots import ChartGenerationPayload

logger = logging.getLogger(__name__)


class ChartSettings:
narrow_chart_width = DEFAULT_CHART_WIDTH
Expand All @@ -24,6 +30,14 @@ def width(self) -> int:
def height(self) -> int:
return self._chart_generation_payload.chart_height

@property
def y_axis_minimum_value(self) -> int | Decimal:
return self._chart_generation_payload.y_axis_minimum_value

@property
def y_axis_maximum_value(self) -> int | Decimal | None:
return self._chart_generation_payload.y_axis_maximum_value

@staticmethod
def get_tick_font_config() -> DICT_OF_STR_ONLY:
return {
Expand Down Expand Up @@ -121,6 +135,53 @@ def get_line_with_shaded_section_chart_config(self):
chart_config["showlegend"] = False
return chart_config

def build_line_single_simplified_y_axis_value_params(
self,
) -> dict[str, list[str | int | Decimal]]:
"""Returns a dictionary containing `y-axis` parameter values
Notes:
if a `y_axis_minimum_value` is provided that is higher than the
lowest value in the `y_axis_values` list then the min value of
`y_axis_values` is used as the lowest number in the y-axis range.
if a `y_axis_maximum_value` is provided that is lower than the
highest value in the `y_axis_values` list then the max value of
`y_axis_values` is used as the highest number in the y-axis range.
Returns:
A dictionary containing the y-axis parameters than make up the
y-axis ticks and tick values.
"""
plots_data = self.plots_data[0]

if self.y_axis_minimum_value < min(plots_data.y_axis_values):
min_value = self.y_axis_minimum_value
else:
min_value = min(plots_data.y_axis_values)
logger.info(
"The minimum value provided was to high, fallen back to the min value in the data"
)

if self.y_axis_maximum_value and (
self.y_axis_maximum_value > max(plots_data.y_axis_values)
):
max_value = self.y_axis_maximum_value
else:
max_value = max(plots_data.y_axis_values)
logger.info(
"The maximum value provided was to low, fallen back to the max value in the data"
)

return {
"y_axis_tick_values": [min_value, max_value],
"y_axis_tick_text": [
return_formatted_min_y_axis_value(y_axis_values=[min_value]),
return_formatted_max_y_axis_value(y_axis_values=[max_value]),
],
"range": [min_value, max_value],
}

def build_line_single_simplified_axis_params(
self,
) -> dict[str, list[str | int | Decimal]]:
Expand All @@ -130,6 +191,9 @@ def build_line_single_simplified_axis_params(
dictionary of parameters for charts settings parameters
"""
plot_data = self.plots_data[0]

y_axis_params = self.build_line_single_simplified_y_axis_value_params()

return {
"x_axis_tick_values": [
plot_data.x_axis_values[0],
Expand All @@ -139,13 +203,12 @@ def build_line_single_simplified_axis_params(
plot_data.x_axis_values[0].strftime("%b, %Y"),
plot_data.x_axis_values[-1].strftime("%b, %Y"),
],
"y_axis_tick_values": [0, max(plot_data.y_axis_values)],
"y_axis_tick_text": [
"0",
return_formatted_max_y_axis_value(
y_axis_values=plot_data.y_axis_values,
),
],
"y_axis_tick_values": y_axis_params["y_axis_tick_values"],
"y_axis_tick_text": y_axis_params["y_axis_tick_text"],
"range": y_axis_params["range"],
"rangemode": (
"tozero" if y_axis_params["y_axis_tick_values"][0] == 0 else "normal"
),
}

def get_line_single_simplified_chart_config(self):
Expand Down Expand Up @@ -178,6 +241,8 @@ def get_line_single_simplified_chart_config(self):
chart_config["yaxis"]["tickfont"][
"color"
] = colour_scheme.RGBAColours.LS_DARK_GREY.stringified
chart_config["yaxis"]["range"] = axis_params["range"]
chart_config["yaxis"]["rangemode"] = axis_params["rangemode"]

return chart_config

Expand Down
41 changes: 41 additions & 0 deletions metrics/domain/charts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,44 @@ def return_formatted_max_y_axis_value(
"""
max_value = _extract_max_value(y_axis_values=y_axis_values)
return convert_large_numbers_to_short_text(number=max_value)


def _extract_min_value(
y_axis_values: list[Decimal],
) -> int:
"""Extracts the lowest `Decimal` value from the `y_axis_values`
list and returns an `Int` rounded to the nearest large number
Notes:
`place_value` is the place of the first digit in the number
represented by the number of digits to its right.
Eg: a number of `1000` has 3 places afer the first digit
so `place_value` of = 3
Args:
y_axis_values: list of Decimal values
Returns:
An integer of the highest value from the list rounded to the
nearest 10, 100, 1000, ... depending on the number provided.
"""
min_y_axis_value = round(min(y_axis_values))
place_value = len(str(min_y_axis_value)) - 1
return round(min_y_axis_value, -place_value)


def return_formatted_min_y_axis_value(
y_axis_values: list[Decimal],
) -> int:
"""Returns the lowest value from `y_axis_values` as a formatted string
Args:
y_axis_values: A list of `Decimal` values representing
the y_axis values of a `Timeseries` chart.
Returns:
A string of the lowest value from `y_axis_values` rounded up
and formatted as a short string Eg: 1400 becomes 1k
"""
min_value = _extract_min_value(y_axis_values=y_axis_values)
return convert_large_numbers_to_short_text(number=min_value)
1 change: 1 addition & 0 deletions metrics/domain/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def get_y_axis_value(cls, *, name: str) -> str:

DEFAULT_X_AXIS = ChartAxisFields.get_default_x_axis().name
DEFAULT_Y_AXIS = ChartAxisFields.get_default_y_axis().name
DEFAULT_Y_AXIS_MINIMUM_VAlUE = 0


class DataSourceFileType(Enum):
Expand Down
5 changes: 5 additions & 0 deletions metrics/domain/models/plots.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
from decimal import Decimal
from typing import Literal, Self

from dateutil.relativedelta import relativedelta
Expand Down Expand Up @@ -162,6 +163,8 @@ class ChartRequestParams(BaseModel):
x_axis_title: str = ""
y_axis: str
y_axis_title: str = ""
y_axis_minimum_value: Decimal = 0
y_axis_maximum_value: Decimal | None = None


class NoReportingDelayPeriodFoundError(Exception): ...
Expand Down Expand Up @@ -242,6 +245,8 @@ class ChartGenerationPayload(BaseModel):
chart_height: int
x_axis_title: str
y_axis_title: str
y_axis_minimum_value: Decimal = 0
y_axis_maximum_value: Decimal | None = None


class CompletePlotData(BaseModel):
Expand Down
2 changes: 2 additions & 0 deletions metrics/interfaces/charts/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ def build_chart_generation_payload(self) -> ChartGenerationPayload:
y_axis_title=self.chart_request_params.y_axis_title,
chart_height=self.chart_request_params.chart_height,
chart_width=self.chart_request_params.chart_width,
y_axis_minimum_value=self.chart_request_params.y_axis_minimum_value,
y_axis_maximum_value=self.chart_request_params.y_axis_maximum_value,
)

def _build_chart_plots_data(self) -> list[PlotGenerationData]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def test_main_layout(self, fake_plot_data: PlotGenerationData):
plots=chart_plots_data,
x_axis_title="",
y_axis_title="",
y_axis_minimum_value=0,
y_axis_maximum_value=None,
)

# When
Expand Down Expand Up @@ -87,6 +89,8 @@ def test_main_bar_plot(self, fake_plot_data: PlotGenerationData):
plots=chart_plots_data,
x_axis_title="",
y_axis_title="",
y_axis_minimum_value=0,
y_axis_maximum_value=None,
)

# When
Expand Down Expand Up @@ -166,6 +170,8 @@ def test_x_axis_type_is_not_date(self, fake_plot_data: PlotGenerationData):
plots=chart_plots_data,
x_axis_title="",
y_axis_title="",
y_axis_minimum_value=0,
y_axis_maximum_value=None,
)

# When
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/metrics/api/serializers/test_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ def test_x_and_y_axis_are_none_into_model(
"plots": chart_plots,
"x_axis": None,
"y_axis": None,
"y_axis_minimum_value": 0,
"y_axis_maximum_value": None,
}
serializer = ChartsSerializer(data=valid_data_payload)

Expand Down Expand Up @@ -621,6 +623,8 @@ def test_to_models_returns_correct_models(self):
"chart_height": 300,
"chart_width": 400,
"plots": chart_plots,
"y_axis_minimum_value": 0,
"y_axis_maximum_value": None,
}
serializer = ChartsSerializer(data=valid_data_payload)

Expand Down Expand Up @@ -668,6 +672,8 @@ def test_valid_payload_with_optional_x_and_y_fields_provided(self):
"plots": chart_plots,
"x_axis": x_axis,
"y_axis": y_axis,
"y_axis_minimum_value": 0,
"y_axis_maximum_value": None,
}
serializer = ChartsSerializer(data=valid_data_payload)

Expand Down
Loading

0 comments on commit 2088894

Please sign in to comment.