Skip to content

Commit

Permalink
CDD-2123: New simplified line chart
Browse files Browse the repository at this point in the history
  • Loading branch information
phill-stanley committed Sep 9, 2024
1 parent 9dda734 commit d188a0c
Show file tree
Hide file tree
Showing 16 changed files with 692 additions and 4 deletions.
2 changes: 1 addition & 1 deletion cms/metrics_interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def get_colours() -> list[tuple[str, str]]:
[("BLUE", "BLUE"), ...]
"""
return RGBAChartLineColours.choices()
return RGBAChartLineColours.selectable_choices()

def get_all_topic_names(self) -> QuerySet:
"""Gets all available topic names as a flat list queryset.
Expand Down
35 changes: 35 additions & 0 deletions metrics/domain/charts/chart_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,41 @@ def get_line_with_shaded_section_chart_config(self):
chart_config["showlegend"] = False
return chart_config

def get_line_single_simplified_chart_config(
self,
x_axis_tick_values: list[int],
x_axis_tick_text: list[str],
y_axis_tick_values: list[int],
y_axis_tick_text: list[str],
):
# Chart Config
chart_config = self.get_base_chart_config()
chart_config["showlegend"] = False
chart_config["margin"]["l"] = 25
chart_config["margin"]["r"] = 25
chart_config["margin"]["pad"] = 25

# x_axis config
chart_config["xaxis"]["ticks"] = "outside"
chart_config["xaxis"]["tickvals"] = x_axis_tick_values
chart_config["xaxis"]["ticktext"] = x_axis_tick_text
chart_config["xaxis"]["ticklen"] = 0
chart_config["xaxis"]["tickfont"][
"color"
] = colour_scheme.RGBAColours.LS_DARK_GREY.stringified

# y_axis config
chart_config["yaxis"]["zeroline"] = False
chart_config["yaxis"]["ticks"] = "outside"
chart_config["yaxis"]["tickvals"] = y_axis_tick_values
chart_config["yaxis"]["ticktext"] = y_axis_tick_text
chart_config["yaxis"]["ticklen"] = 0
chart_config["yaxis"]["tickfont"][
"color"
] = colour_scheme.RGBAColours.LS_DARK_GREY.stringified

return chart_config

def get_bar_chart_config(self):
chart_config = self.get_base_chart_config()
chart_config["barmode"] = "group"
Expand Down
34 changes: 32 additions & 2 deletions metrics/domain/charts/colour_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class RGBAChartLineColours(Enum):
COLOUR_11_KHAKI = 71, 71, 0
COLOUR_12_BLUE = 0, 157, 214

# simplified single line chart colours
TREND_LINE_POSITIVE = 0, 112, 60
TREND_LINE_NEGATIVE = 171, 43, 23

# Legacy colors
RED: RGBA_VALUES = 212, 53, 28
YELLOW: RGBA_VALUES = 255, 221, 0
Expand Down Expand Up @@ -54,8 +58,34 @@ def stringified(self) -> str:
@classmethod
def choices(cls):
return tuple(
(chart_type.name, cls._convert_to_readable_name(chart_type.name))
for chart_type in cls
(
chart_line_color.name,
cls._convert_to_readable_name(chart_line_color.name),
)
for chart_line_color in cls
)

@classmethod
def selectable_choices(cls):
"""Returns chart line colours which are selectable from the CMS
Returns:
Nested tuples of 2 item tuples as expected by the CMS forms
with the value name and a formatted display version
Examples:
(("COLOUR_1_DARK_BLUE", "Colour 1 Dark Blue"), ...)
"""
non_selectable = [
cls.TREND_LINE_NEGATIVE,
cls.TREND_LINE_POSITIVE,
]
return tuple(
(
chart_line_color.name,
cls._convert_to_readable_name(chart_line_color.name),
)
for chart_line_color in cls
if chart_line_color not in non_selectable
)

@classmethod
Expand Down
2 changes: 2 additions & 0 deletions metrics/domain/charts/line_single_simplified/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .generation import generate_chart_figure
from .utils import return_formatted_max_y_axis_value
138 changes: 138 additions & 0 deletions metrics/domain/charts/line_single_simplified/generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from datetime import datetime
from decimal import Decimal

import plotly.graph_objects

from metrics.domain.charts import chart_settings
from metrics.domain.charts.colour_scheme import RGBAChartLineColours
from metrics.domain.models import PlotData

from .utils import return_formatted_max_y_axis_value


def _build_chart_config_params(
x_axis_values: list[datetime.date],
y_axis_values: list[Decimal],
) -> dict[str | int | Decimal]:
"""Creates the parameters for `get_line_single_simplified_chart_config`
Args:
x_axis_values: list of dates for the x_axis of a chart
y_axis_values: list of Decimal values for the y_axis of a chart
Returns:
dictionary of parameters for charts settings parameters
"""
return {
"x_axis_tick_values": [x_axis_values[0], x_axis_values[-1]],
"x_axis_tick_text": [
x_axis_values[0].strftime("%b, %Y"),
x_axis_values[-1].strftime("%b, %Y"),
],
"y_axis_tick_values": [0, max(y_axis_values)],
"y_axis_tick_text": [
"0",
return_formatted_max_y_axis_value(y_axis_values=y_axis_values),
],
}


def create_simplified_line_chart(
*,
plot_data: PlotData,
chart_height: int,
chart_width: int,
x_axis_values: list[str],
y_axis_values: list[Decimal],
) -> plotly.graph_objects.Figure:
"""Creates a `Figure` object for the given `values` as a line graph with a shaded region.
Args:
plot_data: `PlotData` model
chart_height: chart width as an integer
chart_width: chart height as an integer
x_axis_values: list of `datetime.date` objects
y_axis_values: list of `Decimal` values
Returns:
`Figure`: A `Plotly` object which can be
written to a file, or shown.
"""
figure = plotly.graph_objects.Figure()

selected_colour = RGBAChartLineColours.get_colour(
colour=plot_data[0].parameters.line_colour
)

line_shape = "spline" if plot_data[0].parameters.use_smooth_lines else "linear"

line_plot: dict = _create_line_plot(
x_axis_values=x_axis_values,
y_axis_values=y_axis_values,
line_shape=line_shape,
colour=selected_colour.stringified,
)

figure.add_trace(trace=line_plot)

settings = chart_settings.ChartSettings(
width=chart_width, height=chart_height, plots_data=plot_data
)

layout_params = _build_chart_config_params(
x_axis_values=x_axis_values,
y_axis_values=y_axis_values,
)
layout_args = settings.get_line_single_simplified_chart_config(**layout_params)
figure.update_layout(**layout_args)

return figure


def _create_line_plot(
*,
x_axis_values: list[str],
y_axis_values: list[str],
colour: str,
line_shape: str,
) -> dict:
return plotly.graph_objects.Scatter(
x=x_axis_values,
y=y_axis_values,
mode="lines",
line={"color": colour, "width": 3},
line_shape=line_shape,
)


def generate_chart_figure(
*,
plot_data: PlotData,
chart_height: int,
chart_width: int,
x_axis_values: list[datetime.date],
y_axis_values: list[Decimal],
) -> plotly.graph_objects.Figure:
"""Creates a `Figure` object for the given `chart_plots_data` as a
simplified line graph with a single plot and 4 axis ticks
Args:
plot_data: A `PlotData` model
chart_height: The chart height in pixels
chart_width: The chart width in pixels
x_axis_values: A list of datetime.date objects for
the x-axis of a chart
y_axis_values: A list of Decimal values for the
y-axis of a chart
Returns:
`Figure`: A `Plotly` object which can then be
written to a file, or shown.
"""
return create_simplified_line_chart(
plot_data=plot_data,
chart_height=chart_height,
chart_width=chart_width,
x_axis_values=x_axis_values,
y_axis_values=y_axis_values,
)
71 changes: 71 additions & 0 deletions metrics/domain/charts/line_single_simplified/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from decimal import Decimal

SUFFIXES = ["", "k", "m"]
E_NOTATION = [1e0, 1e3, 1e6, 1e9]
CONVERT_LARGE_NUMBERS_VALUE_ERROR = (
"This number is to large to be formatted for the simplified chart."
)


def convert_large_numbers_to_short_text(number: int) -> str:
"""Converts the provided `int` into a short number string.
Args:
number: Integer to be formatted as a string
Returns:
A short number string
Eg: 1000 = 1k, 2500 = 2k, 2690 = 3k, 100,000,000 = 1m
"""
if number >= E_NOTATION[1]:

for index in range(len(E_NOTATION)):
try:
if E_NOTATION[index] <= number < E_NOTATION[index + 1]:
return f"{str(int(number / E_NOTATION[index]))}{SUFFIXES[index]}"

except IndexError:
raise ValueError(CONVERT_LARGE_NUMBERS_VALUE_ERROR)

return str(number)


def _extract_max_value(
y_axis_values: list[Decimal],
) -> int:
"""Extracts the highest `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 after the first digit
so `place_value` = 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.
"""
max_y_axis_value = round(max(y_axis_values))
place_value = len(str(max_y_axis_value)) - 1
return round(max_y_axis_value, -place_value)


def return_formatted_max_y_axis_value(
y_axis_values: list[Decimal],
) -> str:
"""Returns the highest 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 highest value from `y_axis_values` rounded up
and formatted as a short string Eg: 1400 becomes 1k
"""
max_value = _extract_max_value(y_axis_values=y_axis_values)
return convert_large_numbers_to_short_text(number=max_value)
2 changes: 2 additions & 0 deletions metrics/domain/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ChartTypes(Enum):
line_with_shaded_section = "line_with_shaded_section"
bar = "bar"
line_multi_coloured = "line_multi_coloured"
line_single_simplified = "line_single_simplified"

@classmethod
def choices(cls) -> tuple[tuple[str, str], ...]:
Expand All @@ -45,6 +46,7 @@ def selectable_choices(cls) -> tuple[tuple[str, str], ...]:
cls.line_multi_coloured,
cls.bar,
cls.line_with_shaded_section,
cls.line_single_simplified,
)
return tuple((chart_type.value, chart_type.value) for chart_type in selectable)

Expand Down
Loading

0 comments on commit d188a0c

Please sign in to comment.