gov-uk-dashboards 26.8.0__py3-none-any.whl → 26.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gov_uk_dashboards/assets/dashboard.css +177 -0
- gov_uk_dashboards/assets/images/CHASE_icon.svg +17 -0
- gov_uk_dashboards/assets/images/explore_data_logo.svg +87 -0
- gov_uk_dashboards/colours.py +22 -0
- gov_uk_dashboards/components/dash/__init__.py +1 -1
- gov_uk_dashboards/components/dash/banners.py +20 -0
- gov_uk_dashboards/components/dash/context_card.py +472 -53
- gov_uk_dashboards/components/dash/data_quality_banner.py +91 -0
- gov_uk_dashboards/components/dash/download_button.py +0 -30
- gov_uk_dashboards/components/dash/footer.py +72 -29
- gov_uk_dashboards/components/dash/green_button.py +25 -0
- gov_uk_dashboards/components/dash/header.py +44 -0
- gov_uk_dashboards/components/dash/main_content.py +8 -1
- gov_uk_dashboards/components/dash/notification_banner.py +8 -5
- gov_uk_dashboards/components/leaflet/leaflet_choropleth_map.py +66 -27
- gov_uk_dashboards/components/plotly/enums.py +2 -0
- gov_uk_dashboards/components/plotly/stacked_barchart.py +53 -15
- gov_uk_dashboards/components/plotly/time_series_chart.py +140 -15
- gov_uk_dashboards/constants.py +19 -0
- gov_uk_dashboards/formatting/number_formatting.py +7 -0
- gov_uk_dashboards/lib/datetime_functions/datetime_functions.py +85 -0
- gov_uk_dashboards/lib/http_headers.py +3 -2
- gov_uk_dashboards/lib/testing_functions/data_test_assertions.py +3 -3
- gov_uk_dashboards/lib/testing_functions/data_test_helper_functions.py +2 -1
- gov_uk_dashboards/log_kpi.py +37 -0
- gov_uk_dashboards/template.html +1 -1
- gov_uk_dashboards/template.py +6 -0
- {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/METADATA +2 -2
- {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/RECORD +32 -27
- {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/WHEEL +1 -1
- {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/licenses/LICENSE +0 -0
- {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/top_level.txt +0 -0
|
@@ -7,13 +7,13 @@ import polars as pl
|
|
|
7
7
|
from dateutil.relativedelta import relativedelta
|
|
8
8
|
from dash import html
|
|
9
9
|
from dash.development.base_component import Component
|
|
10
|
-
from gov_uk_dashboards.components.dash import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from gov_uk_dashboards.formatting.number_formatting import add_commas
|
|
10
|
+
from gov_uk_dashboards.components.dash import heading2, paragraph
|
|
11
|
+
from gov_uk_dashboards.components.dash.details import details
|
|
12
|
+
from gov_uk_dashboards.formatting.number_formatting import add_commas, format_percentage
|
|
14
13
|
from gov_uk_dashboards.lib.datetime_functions.datetime_functions import (
|
|
15
14
|
convert_date_string_to_text_string,
|
|
16
15
|
)
|
|
16
|
+
from gov_uk_dashboards.lib.datetime_functions.datetime_functions import convert_date
|
|
17
17
|
|
|
18
18
|
from gov_uk_dashboards.constants import (
|
|
19
19
|
CHANGED_FROM_GAP_STYLE,
|
|
@@ -24,7 +24,6 @@ from gov_uk_dashboards.constants import (
|
|
|
24
24
|
METRIC_VALUE,
|
|
25
25
|
PERCENTAGE_CHANGE_FROM_PREV_YEAR,
|
|
26
26
|
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR,
|
|
27
|
-
PREVIOUS_2YEAR,
|
|
28
27
|
PREVIOUS_YEAR,
|
|
29
28
|
TWENTY_NINETEEN,
|
|
30
29
|
TWENTY_NINETEEN_VALUE,
|
|
@@ -67,12 +66,12 @@ def get_rolling_period_context_card(
|
|
|
67
66
|
context_card_data = {
|
|
68
67
|
VALUE: latest_year_data[VALUE][0],
|
|
69
68
|
DATE_VALID: latest_year_data[DATE_VALID][0],
|
|
70
|
-
PERCENTAGE_CHANGE_FROM_PREV_YEAR:
|
|
71
|
-
PERCENTAGE_CHANGE_FROM_PREV_YEAR
|
|
72
|
-
|
|
73
|
-
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR:
|
|
74
|
-
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR
|
|
75
|
-
|
|
69
|
+
PERCENTAGE_CHANGE_FROM_PREV_YEAR: format_percentage(
|
|
70
|
+
latest_year_data[PERCENTAGE_CHANGE_FROM_PREV_YEAR][0]
|
|
71
|
+
),
|
|
72
|
+
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR: format_percentage(
|
|
73
|
+
latest_year_data[PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR][0]
|
|
74
|
+
),
|
|
76
75
|
}
|
|
77
76
|
return html.Div(
|
|
78
77
|
[
|
|
@@ -97,20 +96,6 @@ def _get_rolling_period_data_content_for_x_years(
|
|
|
97
96
|
data[DATE_VALID], abbreviate_month=False, include_year=True
|
|
98
97
|
)
|
|
99
98
|
|
|
100
|
-
current_date = data[DATE_VALID]
|
|
101
|
-
previous_year_datetime = get_a_previous_date(current_date, "previous", False)
|
|
102
|
-
two_years_ago_datetime = get_a_previous_date(current_date, "two_previous", False)
|
|
103
|
-
formatted_year_ago = convert_date_string_to_text_string(
|
|
104
|
-
previous_year_datetime.strftime("%Y-%m-%d"),
|
|
105
|
-
abbreviate_month=False,
|
|
106
|
-
include_year=True,
|
|
107
|
-
)
|
|
108
|
-
formatted_two_years_ago = convert_date_string_to_text_string(
|
|
109
|
-
two_years_ago_datetime.strftime("%Y-%m-%d"),
|
|
110
|
-
abbreviate_month=False,
|
|
111
|
-
include_year=True,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
99
|
return html.Div(
|
|
115
100
|
[
|
|
116
101
|
html.Div(
|
|
@@ -124,21 +109,22 @@ def _get_rolling_period_data_content_for_x_years(
|
|
|
124
109
|
className="govuk-body",
|
|
125
110
|
),
|
|
126
111
|
get_changed_from_content(
|
|
127
|
-
calculated_percentage_change=
|
|
112
|
+
calculated_percentage_change=format_percentage(
|
|
113
|
+
data[PERCENTAGE_CHANGE_FROM_PREV_YEAR]
|
|
114
|
+
),
|
|
128
115
|
use_calculated_percentage_change=True,
|
|
129
116
|
increase_is_positive=True,
|
|
130
|
-
comparison_period_text=
|
|
117
|
+
comparison_period_text="from same period previous year",
|
|
131
118
|
),
|
|
132
119
|
html.Div(
|
|
133
120
|
[
|
|
134
121
|
get_changed_from_content(
|
|
135
|
-
calculated_percentage_change=
|
|
136
|
-
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR
|
|
137
|
-
|
|
122
|
+
calculated_percentage_change=format_percentage(
|
|
123
|
+
data[PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR]
|
|
124
|
+
),
|
|
138
125
|
use_calculated_percentage_change=True,
|
|
139
126
|
increase_is_positive=True,
|
|
140
|
-
comparison_period_text="from same
|
|
141
|
-
f"ending {formatted_two_years_ago}",
|
|
127
|
+
comparison_period_text="from same period a year earlier",
|
|
142
128
|
),
|
|
143
129
|
],
|
|
144
130
|
style=CHANGED_FROM_GAP_STYLE,
|
|
@@ -160,7 +146,6 @@ def get_changed_from_content(
|
|
|
160
146
|
comparison_period_text: str = "",
|
|
161
147
|
use_previous_value_rather_than_change: bool = False,
|
|
162
148
|
use_difference_in_weeks_days: bool = False,
|
|
163
|
-
percentage_change_rounding: int = 1,
|
|
164
149
|
use_calculated_percentage_change: bool = False,
|
|
165
150
|
use_number_rather_than_percentage: bool = False,
|
|
166
151
|
) -> Component:
|
|
@@ -190,8 +175,6 @@ def get_changed_from_content(
|
|
|
190
175
|
use_difference_in_weeks_days (bool, optional): If True, show the difference in
|
|
191
176
|
weeks/days (requires `current_value` and `previous_value` as day counts).
|
|
192
177
|
Defaults to False.
|
|
193
|
-
percentage_change_rounding (int, optional): Decimal places to round percentage change.
|
|
194
|
-
Defaults to 1.
|
|
195
178
|
use_calculated_percentage_change (bool, optional): If True, use the supplied
|
|
196
179
|
`calculated_percentage_change` instead of computing it. Defaults to False.
|
|
197
180
|
use_number_rather_than_percentage (bool, optional): If True, display the change as
|
|
@@ -206,7 +189,7 @@ def get_changed_from_content(
|
|
|
206
189
|
"""
|
|
207
190
|
if use_previous_value_rather_than_change and use_difference_in_weeks_days:
|
|
208
191
|
raise ValueError(
|
|
209
|
-
"
|
|
192
|
+
"use_previous_value_rather_than_change and use_difference_in_weeks_days "
|
|
210
193
|
"both cannot be true"
|
|
211
194
|
)
|
|
212
195
|
if use_calculated_percentage_change:
|
|
@@ -250,6 +233,7 @@ def get_changed_from_content(
|
|
|
250
233
|
# which is added from govuk-tag class
|
|
251
234
|
)
|
|
252
235
|
)
|
|
236
|
+
print("PREVIOUS VAKUE", previous_value)
|
|
253
237
|
content.append(
|
|
254
238
|
html.Span(
|
|
255
239
|
f"{previous_value}{unit}",
|
|
@@ -290,7 +274,7 @@ def get_changed_from_content(
|
|
|
290
274
|
)
|
|
291
275
|
content.append(
|
|
292
276
|
html.Span(
|
|
293
|
-
f"{
|
|
277
|
+
f"{format_percentage(abs(percentage_change))}{unit}",
|
|
294
278
|
className="govuk-body-s govuk-!-margin-bottom-0 govuk-!-margin-right-1 "
|
|
295
279
|
+ "changed-from-number-formatting",
|
|
296
280
|
)
|
|
@@ -343,7 +327,6 @@ def get_data_for_context_card(
|
|
|
343
327
|
measure: str,
|
|
344
328
|
df: pl.DataFrame,
|
|
345
329
|
value_column: str = VALUE,
|
|
346
|
-
include_data_from_2_years_ago: bool = False,
|
|
347
330
|
display_value_as_int: bool = False,
|
|
348
331
|
abbreviate_month: bool = True,
|
|
349
332
|
include_percentage_change: bool = False,
|
|
@@ -358,8 +341,6 @@ def get_data_for_context_card(
|
|
|
358
341
|
measure (str): The measure for which data is to be fetched.
|
|
359
342
|
df (pl.DataFrame): The dataframe to fetch the measure from.
|
|
360
343
|
value_column (str): The name of the column to get the value for.
|
|
361
|
-
include_data_from_2_years_ago (bool): Whether to include data from 2 years ago. Defaults
|
|
362
|
-
to False.
|
|
363
344
|
display_value_as_int (bool): Whether to display the value as an int. Defaults to False.
|
|
364
345
|
abbreviate_month (bool): Whether to abbreviate the month. Defaults to True.
|
|
365
346
|
include_percentage_change (bool): Whether to include percentage change from previous year
|
|
@@ -409,19 +390,6 @@ def get_data_for_context_card(
|
|
|
409
390
|
TWENTY_NINETEEN: {METRIC_VALUE: twenty_nineteen_data},
|
|
410
391
|
}
|
|
411
392
|
|
|
412
|
-
if include_data_from_2_years_ago:
|
|
413
|
-
date_2_years_ago = get_a_previous_date(previous_year_date, "previous")
|
|
414
|
-
data_from_2_years_ago = get_latest_data_for_year(
|
|
415
|
-
df_measure,
|
|
416
|
-
date_2_years_ago,
|
|
417
|
-
value_column,
|
|
418
|
-
abbreviate_month,
|
|
419
|
-
data_expected_for_previous_year_and_previous_2years,
|
|
420
|
-
include_percentage_change,
|
|
421
|
-
previous_year_date,
|
|
422
|
-
)
|
|
423
|
-
data_to_return = {**data_to_return, PREVIOUS_2YEAR: data_from_2_years_ago}
|
|
424
|
-
|
|
425
393
|
return data_to_return
|
|
426
394
|
|
|
427
395
|
|
|
@@ -451,6 +419,9 @@ def get_a_previous_date(
|
|
|
451
419
|
return new_date
|
|
452
420
|
|
|
453
421
|
|
|
422
|
+
# if include_data_from_2_years_ago:
|
|
423
|
+
|
|
424
|
+
|
|
454
425
|
def get_latest_data_for_year(
|
|
455
426
|
df_measure: pl.DataFrame,
|
|
456
427
|
date: str,
|
|
@@ -557,3 +528,451 @@ def get_latest_data_for_year(
|
|
|
557
528
|
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR
|
|
558
529
|
)[0]
|
|
559
530
|
return output
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# pylint: disable=too-many-instance-attributes
|
|
534
|
+
# pylint: disable=too-few-public-methods
|
|
535
|
+
class ContextCard:
|
|
536
|
+
"""Context card class"""
|
|
537
|
+
|
|
538
|
+
def __init__(
|
|
539
|
+
self,
|
|
540
|
+
df: pl.DataFrame,
|
|
541
|
+
measure: str,
|
|
542
|
+
title: str,
|
|
543
|
+
date_prefix: str,
|
|
544
|
+
units: str = None,
|
|
545
|
+
headline_figure_is_percentage: bool = False,
|
|
546
|
+
additional_text_and_position: tuple[str, int] = None,
|
|
547
|
+
date_format: str = "%d %b %Y",
|
|
548
|
+
use_previous_value_rather_than_change: bool = False, # rename????
|
|
549
|
+
use_difference_in_weeks_days: bool = False,
|
|
550
|
+
increase_is_positive: bool = True,
|
|
551
|
+
use_number_for_change_rather_than_percentage: bool = False, # rename????
|
|
552
|
+
details_summary_and_text: tuple[str, str] = None,
|
|
553
|
+
):
|
|
554
|
+
"""
|
|
555
|
+
A compact “context card” for a time-series measure that renders:
|
|
556
|
+
• a headline figure,
|
|
557
|
+
• a date label,
|
|
558
|
+
• up to two comparison tags (previous year, two years ago),
|
|
559
|
+
• optional title, units, inline note, and a collapsible details section.
|
|
560
|
+
|
|
561
|
+
The component supports three comparison display modes:
|
|
562
|
+
1) percent change (default),
|
|
563
|
+
2) show the comparison period’s value instead of percent change,
|
|
564
|
+
3) show the time difference in weeks/days (for duration measures).
|
|
565
|
+
|
|
566
|
+
----------
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
df : pl.DataFrame
|
|
570
|
+
Input data for one or more measures across dates. Expected columns:
|
|
571
|
+
- MEASURE (categorical/str): identifies the measure.
|
|
572
|
+
- DATE_VALID (date or ISO string): observation date.
|
|
573
|
+
- VALUE (numeric): observed value for the measure.
|
|
574
|
+
|
|
575
|
+
Two data shapes are supported by the current implementation:
|
|
576
|
+
|
|
577
|
+
A) “Precomputed change” mode:
|
|
578
|
+
If the dataframe contains a column literally named
|
|
579
|
+
"Percentage change from prev year", `_filter_df` keeps only the latest
|
|
580
|
+
DATE_VALID row for the selected measure. In `_get_changed_from_content`,
|
|
581
|
+
percent-change tags are read directly from:
|
|
582
|
+
- PERCENTAGE_CHANGE_FROM_PREV_YEAR
|
|
583
|
+
- PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR
|
|
584
|
+
|
|
585
|
+
B) “Computed change” mode:
|
|
586
|
+
Otherwise, three dates are selected for the measure:
|
|
587
|
+
latest_date,
|
|
588
|
+
previous_date = get_a_previous_date(latest_date),
|
|
589
|
+
year_earlier_date = get_a_previous_date(previous_date),
|
|
590
|
+
then the frame is filtered to those dates and sorted descending.
|
|
591
|
+
Percent changes are computed from the numeric VALUEs in positions:
|
|
592
|
+
[0] latest, [1] previous year, [2] two years ago
|
|
593
|
+
|
|
594
|
+
measure : str
|
|
595
|
+
The measure name to filter from `df[MEASURE]`.
|
|
596
|
+
|
|
597
|
+
title : str
|
|
598
|
+
Optional title shown at the top of the card. If falsy, no title is rendered.
|
|
599
|
+
|
|
600
|
+
date_prefix : str
|
|
601
|
+
Text prefixed before the current date (e.g., "Data to", "Week ending").
|
|
602
|
+
|
|
603
|
+
units : str, optional
|
|
604
|
+
Units label inserted beneath the headline figure.
|
|
605
|
+
|
|
606
|
+
headline_figure_is_percentage : bool, default False
|
|
607
|
+
Controls headline formatting and some tag formatting.
|
|
608
|
+
If True, the headline uses `format_percentage(abs(value))` and appends "%".
|
|
609
|
+
If False, the headline uses `add_commas(value, remove_decimal_places=True)`.
|
|
610
|
+
|
|
611
|
+
additional_text_and_position : tuple[str, int], optional
|
|
612
|
+
An extra paragraph inserted into the content at the given index:
|
|
613
|
+
(text, position_index).
|
|
614
|
+
|
|
615
|
+
date_format : str, default "%d %b %Y"
|
|
616
|
+
Format string for the latest DATE_VALID (e.g., "05 Jan 2026").
|
|
617
|
+
|
|
618
|
+
use_previous_value_rather_than_change : bool, default False
|
|
619
|
+
When True, comparison tags show the comparison period’s VALUE instead of a
|
|
620
|
+
percent change (e.g., "up from 1,234 from previous year").
|
|
621
|
+
NOTE: This cannot be True at the same time as `use_difference_in_weeks_days`.
|
|
622
|
+
|
|
623
|
+
use_difference_in_weeks_days : bool, default False
|
|
624
|
+
When True, comparison tags show the difference between the current VALUE and
|
|
625
|
+
the comparison VALUE converted to weeks/days via `convert_days_to_weeks_and_days`.
|
|
626
|
+
Tag prefix becomes:
|
|
627
|
+
- "longer than ..." if percent-change is positive,
|
|
628
|
+
- "shorter than ..." if percent-change is negative,
|
|
629
|
+
- "unchanged from ..." if zero.
|
|
630
|
+
Requires both current and comparison values to be present.
|
|
631
|
+
NOTE: This cannot be True at the same time as `use_previous_value_rather_than_change`.
|
|
632
|
+
|
|
633
|
+
increase_is_positive : bool, default True
|
|
634
|
+
Controls the semantic mapping of change to colour/arrow:
|
|
635
|
+
- If True: increase → green ↑, decrease → red ↓
|
|
636
|
+
- If False: increase → red ↓, decrease → green ↑
|
|
637
|
+
Zero change renders a neutral (grey/right) style.
|
|
638
|
+
|
|
639
|
+
use_number_for_change_rather_than_percentage : bool, default False
|
|
640
|
+
Only affects the “previous-value” tag mode:
|
|
641
|
+
- If True and the comparison period is "previous year", the label text is
|
|
642
|
+
changed from "previous year" to "in previous year".
|
|
643
|
+
(No other behaviour is currently toggled by this flag in the present code.)
|
|
644
|
+
|
|
645
|
+
details_summary_and_text : tuple[str, str], optional
|
|
646
|
+
Renders a collapsible details section at the bottom with
|
|
647
|
+
(summary_text, details_body).
|
|
648
|
+
|
|
649
|
+
----------
|
|
650
|
+
Behaviour summary
|
|
651
|
+
----------
|
|
652
|
+
• The dataframe is filtered to `measure` and reduced to either:
|
|
653
|
+
- one latest row (if the literal column "Percentage change from prev year" exists), or
|
|
654
|
+
- three rows for the latest + two prior comparison dates (computed via
|
|
655
|
+
`get_a_previous_date`).
|
|
656
|
+
|
|
657
|
+
• Headline figure:
|
|
658
|
+
- If `use_difference_in_weeks_days` is True: `convert_days_to_weeks_and_days(VALUE[0])`
|
|
659
|
+
- Else if `headline_figure_is_percentage` is True:
|
|
660
|
+
`format_percentage(abs(VALUE[0])) + "%"`
|
|
661
|
+
- Else: `add_commas(VALUE[0], remove_decimal_places=True)`
|
|
662
|
+
|
|
663
|
+
• Date label:
|
|
664
|
+
Uses the latest DATE_VALID formatted via `convert_date(..., "%Y-%m-%d", date_format)`.
|
|
665
|
+
|
|
666
|
+
• Comparison tags:
|
|
667
|
+
Attempts to render up to two tags: vs "previous year" and vs "two years ago".
|
|
668
|
+
Tags are omitted if the required inputs are missing.
|
|
669
|
+
|
|
670
|
+
• Mutual exclusivity:
|
|
671
|
+
`use_previous_value_rather_than_change` and `use_difference_in_weeks_days`
|
|
672
|
+
cannot both be True (ValueError raised when building tag content).
|
|
673
|
+
|
|
674
|
+
• Layout:
|
|
675
|
+
Content order is:
|
|
676
|
+
[title?], headline, [units?], (date_prefix + date), comparison tags, [details?]
|
|
677
|
+
`additional_text_and_position` inserts an extra paragraph at the specified index.
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
self.measure = measure
|
|
681
|
+
self.title = title
|
|
682
|
+
self.units = units
|
|
683
|
+
self.headline_figure_is_percentage = headline_figure_is_percentage
|
|
684
|
+
self.additional_text_and_position = additional_text_and_position
|
|
685
|
+
self.date_prefix = date_prefix
|
|
686
|
+
self.date_format = date_format
|
|
687
|
+
self.use_previous_value_rather_than_change = (
|
|
688
|
+
use_previous_value_rather_than_change
|
|
689
|
+
)
|
|
690
|
+
self.use_difference_in_weeks_days = use_difference_in_weeks_days
|
|
691
|
+
self.increase_is_positive = increase_is_positive
|
|
692
|
+
self.use_number_for_change_rather_than_percentage = (
|
|
693
|
+
use_number_for_change_rather_than_percentage
|
|
694
|
+
)
|
|
695
|
+
self.df = self._filter_df(df)
|
|
696
|
+
self.headline_figure = self._get_headline_figure()
|
|
697
|
+
self.current_date = self._get_current_date()
|
|
698
|
+
self.details_summary_and_text = details_summary_and_text
|
|
699
|
+
|
|
700
|
+
def __call__(self):
|
|
701
|
+
card_content = [
|
|
702
|
+
html.Div(
|
|
703
|
+
self.headline_figure,
|
|
704
|
+
className="govuk-body govuk-!-font-weight-bold",
|
|
705
|
+
style=LARGE_BOLD_FONT_STYLE | {"marginBottom": "0px"},
|
|
706
|
+
),
|
|
707
|
+
paragraph(f"{self.date_prefix} {self.current_date}"),
|
|
708
|
+
self._get_changed_from_content(),
|
|
709
|
+
]
|
|
710
|
+
if self.title:
|
|
711
|
+
card_content.insert(0, heading2(self.title))
|
|
712
|
+
if self.additional_text_and_position:
|
|
713
|
+
card_content.insert(
|
|
714
|
+
self.additional_text_and_position[1],
|
|
715
|
+
paragraph(self.additional_text_and_position[0]),
|
|
716
|
+
)
|
|
717
|
+
if self.details_summary_and_text:
|
|
718
|
+
card_content.append(
|
|
719
|
+
html.Div(
|
|
720
|
+
[
|
|
721
|
+
details(
|
|
722
|
+
self.details_summary_and_text[0],
|
|
723
|
+
self.details_summary_and_text[1],
|
|
724
|
+
)
|
|
725
|
+
],
|
|
726
|
+
style={"marginTop": "40px"}, # from h repo,
|
|
727
|
+
)
|
|
728
|
+
)
|
|
729
|
+
if self.units:
|
|
730
|
+
card_content.insert(
|
|
731
|
+
1, html.Div(paragraph(self.units), style={"marginTop": "-15px"})
|
|
732
|
+
)
|
|
733
|
+
card_for_display = html.Div(
|
|
734
|
+
card_content,
|
|
735
|
+
className="context-card-grid-item",
|
|
736
|
+
)
|
|
737
|
+
return card_for_display
|
|
738
|
+
|
|
739
|
+
def _filter_df(self, df):
|
|
740
|
+
df_for_measure = df.filter(df[MEASURE] == self.measure)
|
|
741
|
+
latest_date = df_for_measure.select(pl.col(DATE_VALID).max()).item()
|
|
742
|
+
|
|
743
|
+
if "Percentage change from prev year" in df_for_measure.columns:
|
|
744
|
+
return df_for_measure.filter(pl.col(DATE_VALID) == latest_date)
|
|
745
|
+
|
|
746
|
+
previous_date = get_a_previous_date(latest_date)
|
|
747
|
+
year_earlier_date = get_a_previous_date(previous_date)
|
|
748
|
+
|
|
749
|
+
if not self.use_difference_in_weeks_days and not (
|
|
750
|
+
self.use_number_for_change_rather_than_percentage
|
|
751
|
+
and self.use_previous_value_rather_than_change
|
|
752
|
+
):
|
|
753
|
+
df_for_measure = df_for_measure.with_columns(pl.col(VALUE).cast(pl.Int64))
|
|
754
|
+
|
|
755
|
+
return df_for_measure.filter(
|
|
756
|
+
pl.col(DATE_VALID).is_in([latest_date, previous_date, year_earlier_date])
|
|
757
|
+
).sort(DATE_VALID, descending=True)
|
|
758
|
+
|
|
759
|
+
def _get_headline_figure(self):
|
|
760
|
+
unit = "%" if self.headline_figure_is_percentage else ""
|
|
761
|
+
|
|
762
|
+
return (
|
|
763
|
+
(
|
|
764
|
+
f"{str(format_percentage(abs(self.df[VALUE][0])))+unit}"
|
|
765
|
+
if self.headline_figure_is_percentage
|
|
766
|
+
else add_commas(self.df[VALUE][0], remove_decimal_places=True) + unit
|
|
767
|
+
)
|
|
768
|
+
if not self.use_difference_in_weeks_days
|
|
769
|
+
else convert_days_to_weeks_and_days(self.df[VALUE][0])
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# pylint: disable=too-many-statements
|
|
773
|
+
def _get_current_date(self):
|
|
774
|
+
current_date = self.df[DATE_VALID][0]
|
|
775
|
+
return convert_date(current_date, "%Y-%m-%d", self.date_format)
|
|
776
|
+
|
|
777
|
+
def _get_changed_from_content(self):
|
|
778
|
+
if (
|
|
779
|
+
self.use_previous_value_rather_than_change
|
|
780
|
+
and self.use_difference_in_weeks_days
|
|
781
|
+
):
|
|
782
|
+
raise ValueError(
|
|
783
|
+
"use_previous_value_rather_than_change and use_difference_in_weeks_days "
|
|
784
|
+
"both cannot be true"
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
# ---- helpers ----
|
|
788
|
+
def _scalar(x):
|
|
789
|
+
"""Polars -> python scalar (handles Series/Expr-ish values)."""
|
|
790
|
+
if x is None:
|
|
791
|
+
return None
|
|
792
|
+
if isinstance(x, pl.Series):
|
|
793
|
+
return x.item() if len(x) else None
|
|
794
|
+
return x
|
|
795
|
+
|
|
796
|
+
def _build_tag(
|
|
797
|
+
*,
|
|
798
|
+
percentage_change: float | None,
|
|
799
|
+
comparison_period_text: str,
|
|
800
|
+
current_value: float | None = None,
|
|
801
|
+
previous_value: float | None = None,
|
|
802
|
+
):
|
|
803
|
+
if percentage_change is None:
|
|
804
|
+
return None
|
|
805
|
+
increase_is_positive = getattr(self, "increase_is_positive", True)
|
|
806
|
+
|
|
807
|
+
if percentage_change > 0:
|
|
808
|
+
colour = "green" if increase_is_positive else "red"
|
|
809
|
+
arrow_direction = "up"
|
|
810
|
+
prefix = "up"
|
|
811
|
+
elif percentage_change < 0:
|
|
812
|
+
colour = "red" if increase_is_positive else "green"
|
|
813
|
+
arrow_direction = "down"
|
|
814
|
+
prefix = "down"
|
|
815
|
+
else:
|
|
816
|
+
colour = "grey"
|
|
817
|
+
arrow_direction = "right"
|
|
818
|
+
prefix = ""
|
|
819
|
+
|
|
820
|
+
box_style_class = (
|
|
821
|
+
f"govuk-tag govuk-tag--{colour} changed-from-box-formatting"
|
|
822
|
+
)
|
|
823
|
+
if percentage_change != 0:
|
|
824
|
+
box_style_class += f" changed-from-arrow_{arrow_direction}_{colour}"
|
|
825
|
+
|
|
826
|
+
unit = (
|
|
827
|
+
"%"
|
|
828
|
+
if (
|
|
829
|
+
self.headline_figure_is_percentage
|
|
830
|
+
and self.use_previous_value_rather_than_change
|
|
831
|
+
)
|
|
832
|
+
or not self.use_previous_value_rather_than_change
|
|
833
|
+
else ""
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
content = []
|
|
837
|
+
|
|
838
|
+
# Option A: show previous value rather than % change
|
|
839
|
+
if self.use_previous_value_rather_than_change:
|
|
840
|
+
if previous_value is None:
|
|
841
|
+
return None
|
|
842
|
+
if self.headline_figure_is_percentage:
|
|
843
|
+
previous_value = str(format_percentage(abs(previous_value)))
|
|
844
|
+
else:
|
|
845
|
+
previous_value = add_commas(
|
|
846
|
+
previous_value, remove_decimal_places=True
|
|
847
|
+
)
|
|
848
|
+
content.append(
|
|
849
|
+
html.Span(
|
|
850
|
+
(
|
|
851
|
+
f"{prefix} from "
|
|
852
|
+
if percentage_change != 0
|
|
853
|
+
else "unchanged from "
|
|
854
|
+
),
|
|
855
|
+
className="govuk-body-s govuk-!-margin-bottom-0 text-color-inherit "
|
|
856
|
+
"text-no-transform",
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
content.append(
|
|
860
|
+
html.Span(
|
|
861
|
+
f"{previous_value}{unit}",
|
|
862
|
+
className="govuk-body-s govuk-!-margin-bottom-0 govuk-!-margin-right-1 "
|
|
863
|
+
"changed-from-number-formatting",
|
|
864
|
+
)
|
|
865
|
+
)
|
|
866
|
+
if self.use_number_for_change_rather_than_percentage:
|
|
867
|
+
if comparison_period_text == "previous year":
|
|
868
|
+
comparison_period_text = "in " + comparison_period_text
|
|
869
|
+
|
|
870
|
+
# Option B: show difference in weeks/days (requires values)
|
|
871
|
+
elif self.use_difference_in_weeks_days:
|
|
872
|
+
if current_value is None or previous_value is None:
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
difference_in_weeks_and_days = convert_days_to_weeks_and_days(
|
|
876
|
+
current_value - previous_value
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
if percentage_change > 0:
|
|
880
|
+
comparison_period_text_prefix = "longer than "
|
|
881
|
+
elif percentage_change < 0:
|
|
882
|
+
comparison_period_text_prefix = "shorter than "
|
|
883
|
+
else:
|
|
884
|
+
comparison_period_text_prefix = "unchanged from "
|
|
885
|
+
|
|
886
|
+
content.append(
|
|
887
|
+
html.Span(
|
|
888
|
+
f"{difference_in_weeks_and_days}",
|
|
889
|
+
className="govuk-body-s govuk-!-margin-bottom-0 govuk-!-margin-right-1 "
|
|
890
|
+
"changed-from-number-formatting text-no-transform",
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
comparison_period_text = (
|
|
895
|
+
comparison_period_text_prefix + comparison_period_text
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
# Option C: default (% change)
|
|
899
|
+
else:
|
|
900
|
+
content.append(
|
|
901
|
+
html.Span(
|
|
902
|
+
f"{prefix} " if percentage_change != 0 else "unchanged from ",
|
|
903
|
+
className="govuk-body-s govuk-!-margin-bottom-0 text-color-inherit "
|
|
904
|
+
"text-no-transform",
|
|
905
|
+
)
|
|
906
|
+
)
|
|
907
|
+
content.append(
|
|
908
|
+
html.Span(
|
|
909
|
+
f"{format_percentage(abs(percentage_change))}{unit}",
|
|
910
|
+
className="govuk-body-s govuk-!-margin-bottom-0 govuk-!-margin-right-1 "
|
|
911
|
+
"changed-from-number-formatting",
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
comparison_period_text = "from " + comparison_period_text
|
|
915
|
+
|
|
916
|
+
content.append(
|
|
917
|
+
html.Span(
|
|
918
|
+
comparison_period_text,
|
|
919
|
+
className="govuk-body-s govuk-!-margin-bottom-0 text-color-inherit "
|
|
920
|
+
"text-no-transform",
|
|
921
|
+
)
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
return html.Div(
|
|
925
|
+
[html.Div(content, className=box_style_class)],
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
# ---- compute changes ----
|
|
929
|
+
current_value = None
|
|
930
|
+
prev_year_value = None
|
|
931
|
+
two_year_value = None
|
|
932
|
+
|
|
933
|
+
if self.df.height == 1:
|
|
934
|
+
# Using provided columns (likely already computed upstream)
|
|
935
|
+
pct_year = _scalar(self.df[PERCENTAGE_CHANGE_FROM_PREV_YEAR])
|
|
936
|
+
pct_2yr = _scalar(self.df[PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR])
|
|
937
|
+
|
|
938
|
+
# If you want to support previous-value/weeks-days in height==1 mode,
|
|
939
|
+
# you'd need extra columns for those values. Otherwise tags will return None for those
|
|
940
|
+
# modes.
|
|
941
|
+
else:
|
|
942
|
+
# Expect order: [latest, prev_year, two_year]
|
|
943
|
+
current_value = _scalar(self.df[VALUE][0])
|
|
944
|
+
prev_year_value = _scalar(self.df[VALUE][1])
|
|
945
|
+
two_year_value = _scalar(self.df[VALUE][2])
|
|
946
|
+
|
|
947
|
+
pct_year = (
|
|
948
|
+
None
|
|
949
|
+
if not prev_year_value
|
|
950
|
+
else ((current_value - prev_year_value) / prev_year_value) * 100
|
|
951
|
+
)
|
|
952
|
+
pct_2yr = (
|
|
953
|
+
None
|
|
954
|
+
if not two_year_value
|
|
955
|
+
else ((current_value - two_year_value) / two_year_value) * 100
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
# ---- build two tags ----
|
|
959
|
+
tag_last_year = _build_tag(
|
|
960
|
+
percentage_change=pct_year,
|
|
961
|
+
comparison_period_text="previous year",
|
|
962
|
+
current_value=current_value,
|
|
963
|
+
previous_value=prev_year_value,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
tag_two_years = _build_tag(
|
|
967
|
+
percentage_change=pct_2yr,
|
|
968
|
+
comparison_period_text="two years ago",
|
|
969
|
+
current_value=current_value,
|
|
970
|
+
previous_value=two_year_value,
|
|
971
|
+
)
|
|
972
|
+
styled_tag_two_years = (
|
|
973
|
+
html.Div(tag_two_years, style=CHANGED_FROM_GAP_STYLE)
|
|
974
|
+
if tag_two_years is not None
|
|
975
|
+
else None
|
|
976
|
+
)
|
|
977
|
+
tags = [t for t in (tag_last_year, styled_tag_two_years) if t is not None]
|
|
978
|
+
return html.Div(tags) if tags else None
|