gov-uk-dashboards 26.24.0__tar.gz → 26.26.0__tar.gz
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-26.24.0/gov_uk_dashboards.egg-info → gov_uk_dashboards-26.26.0}/PKG-INFO +1 -1
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/context_card.py +457 -25
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/plotly/time_series_chart.py +83 -11
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/datetime_functions/datetime_functions.py +85 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0/gov_uk_dashboards.egg-info}/PKG-INFO +1 -1
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/setup.py +1 -1
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/LICENSE +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/MANIFEST.in +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/README.md +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/attach-event-to-dash.js +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/custom_map_style_functions.js +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/dashboard.css +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/download-map.js +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/fonts/bold-affa96571d-v2.woff +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/fonts/bold-b542beb274-v2.woff2 +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/fonts/light-94a07e06a1-v2.woff2 +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/fonts/light-f591b13f7d-v2.woff +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/get_assets_folder.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/govuk-frontend-3.14.0.min.js +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/CHASE_icon.svg +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/DLUHC_WHITE_Master_AW_sm.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/MHCLG-favicon.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/MHCLG_favicon.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/dcms_coatofarms.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/dluhc_favicon.ico +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/explore_data_logo.svg +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/gov_favicon.ico +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/govuk-crest.svg +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/hm-government-logo.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/mhclg_coat_of_arms.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/mhclg_white_no_background.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/oflog/MedianAbsolute.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/oflog/how_to_1_selecting_data.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/oflog/how_to_2_viewing_tabs.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/oflog/how_to_3_viewing_charts.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/oflog/how_to_4_viewing_trends.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/images/oflog/how_to_5_viewing_tables.png +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/index.html +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/mobile-nav.js +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/register_maps +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/scripts.js +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/topojson/usa_110m.json +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/assets/topojson/world_110m.json +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/colours.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/apply_and_reset_filters_buttons.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/banners.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/card.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/card_full_width.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/collapsible_panel.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/comparison_la_filter_button.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/context_banner.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/dashboard_container.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/data_quality_banner.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/details.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/download_button.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/filter_panel.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/footer.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/graph.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/green_button.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/header.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/heading.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/home_page_link_button.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/html_list.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/key_value_pair.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/main_content.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/navbar.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/no_data_message.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/notification_banner.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/paragraph.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/phase_banner.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/row_component.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/side_navbar.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/table.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/tooltip.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/tooltip_title.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/visualisation_commentary.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/visualisation_title.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/dash/warning_text.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/helpers/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/helpers/display_chart_or_table_with_header.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/helpers/generate_dash_graph_from_figure.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/helpers/get_chart_for_download.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/helpers/plotting_helper_functions.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/helpers/update_layout_bgcolor_margin.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/leaflet/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/leaflet/leaflet_choropleth_map.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/plotly/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/plotly/captioned_figure.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/plotly/choropleth_map.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/plotly/enums.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/components/plotly/stacked_barchart.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/constants.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/figures/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/figures/enums/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/figures/enums/dash_patterns.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/figures/line_chart.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/figures/styles/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/figures/styles/line_style.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/formatting/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/formatting/human_readable.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/formatting/number_formatting.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/formatting/round_and_add_prefix_and_suffix.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/formatting/rounding.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/formatting/text_functions.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/dap/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/dap/dap_deployment.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/dap/get_dataframe_from_cds.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/datetime_functions/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/download_functions/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/download_functions/convert_fig_to_image_and_download.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/download_functions/download_csv_with_headers.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/enable_basic_auth.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/http_headers.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/logging.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/testing_functions/__init__.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/testing_functions/barchart_data_test_assertions.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/testing_functions/data_test_assertions.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/testing_functions/data_test_helper_functions.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/testing_functions/timeseries_data_test_assertions.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/lib/warning_text_sensitive.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/log_kpi.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/symbols.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/template.html +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards/template.py +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards.egg-info/SOURCES.txt +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards.egg-info/dependency_links.txt +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards.egg-info/requires.txt +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/gov_uk_dashboards.egg-info/top_level.txt +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/pyproject.toml +0 -0
- {gov_uk_dashboards-26.24.0 → gov_uk_dashboards-26.26.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gov_uk_dashboards
|
|
3
|
-
Version: 26.
|
|
3
|
+
Version: 26.26.0
|
|
4
4
|
Summary: Provides access to functionality common to creating a data dashboard.
|
|
5
5
|
Author: Department for Levelling Up, Housing and Communities
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -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
|
-
)
|
|
10
|
+
from gov_uk_dashboards.components.dash import heading2, paragraph
|
|
11
|
+
from gov_uk_dashboards.components.dash.details import details
|
|
13
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,
|
|
@@ -147,7 +146,6 @@ def get_changed_from_content(
|
|
|
147
146
|
comparison_period_text: str = "",
|
|
148
147
|
use_previous_value_rather_than_change: bool = False,
|
|
149
148
|
use_difference_in_weeks_days: bool = False,
|
|
150
|
-
percentage_change_rounding: int = 1,
|
|
151
149
|
use_calculated_percentage_change: bool = False,
|
|
152
150
|
use_number_rather_than_percentage: bool = False,
|
|
153
151
|
) -> Component:
|
|
@@ -177,8 +175,6 @@ def get_changed_from_content(
|
|
|
177
175
|
use_difference_in_weeks_days (bool, optional): If True, show the difference in
|
|
178
176
|
weeks/days (requires `current_value` and `previous_value` as day counts).
|
|
179
177
|
Defaults to False.
|
|
180
|
-
percentage_change_rounding (int, optional): Decimal places to round percentage change.
|
|
181
|
-
Defaults to 1.
|
|
182
178
|
use_calculated_percentage_change (bool, optional): If True, use the supplied
|
|
183
179
|
`calculated_percentage_change` instead of computing it. Defaults to False.
|
|
184
180
|
use_number_rather_than_percentage (bool, optional): If True, display the change as
|
|
@@ -193,7 +189,7 @@ def get_changed_from_content(
|
|
|
193
189
|
"""
|
|
194
190
|
if use_previous_value_rather_than_change and use_difference_in_weeks_days:
|
|
195
191
|
raise ValueError(
|
|
196
|
-
"
|
|
192
|
+
"use_previous_value_rather_than_change and use_difference_in_weeks_days "
|
|
197
193
|
"both cannot be true"
|
|
198
194
|
)
|
|
199
195
|
if use_calculated_percentage_change:
|
|
@@ -237,6 +233,7 @@ def get_changed_from_content(
|
|
|
237
233
|
# which is added from govuk-tag class
|
|
238
234
|
)
|
|
239
235
|
)
|
|
236
|
+
print("PREVIOUS VAKUE", previous_value)
|
|
240
237
|
content.append(
|
|
241
238
|
html.Span(
|
|
242
239
|
f"{previous_value}{unit}",
|
|
@@ -277,7 +274,7 @@ def get_changed_from_content(
|
|
|
277
274
|
)
|
|
278
275
|
content.append(
|
|
279
276
|
html.Span(
|
|
280
|
-
f"{
|
|
277
|
+
f"{format_percentage(abs(percentage_change))}{unit}",
|
|
281
278
|
className="govuk-body-s govuk-!-margin-bottom-0 govuk-!-margin-right-1 "
|
|
282
279
|
+ "changed-from-number-formatting",
|
|
283
280
|
)
|
|
@@ -330,7 +327,6 @@ def get_data_for_context_card(
|
|
|
330
327
|
measure: str,
|
|
331
328
|
df: pl.DataFrame,
|
|
332
329
|
value_column: str = VALUE,
|
|
333
|
-
include_data_from_2_years_ago: bool = False,
|
|
334
330
|
display_value_as_int: bool = False,
|
|
335
331
|
abbreviate_month: bool = True,
|
|
336
332
|
include_percentage_change: bool = False,
|
|
@@ -345,8 +341,6 @@ def get_data_for_context_card(
|
|
|
345
341
|
measure (str): The measure for which data is to be fetched.
|
|
346
342
|
df (pl.DataFrame): The dataframe to fetch the measure from.
|
|
347
343
|
value_column (str): The name of the column to get the value for.
|
|
348
|
-
include_data_from_2_years_ago (bool): Whether to include data from 2 years ago. Defaults
|
|
349
|
-
to False.
|
|
350
344
|
display_value_as_int (bool): Whether to display the value as an int. Defaults to False.
|
|
351
345
|
abbreviate_month (bool): Whether to abbreviate the month. Defaults to True.
|
|
352
346
|
include_percentage_change (bool): Whether to include percentage change from previous year
|
|
@@ -396,19 +390,6 @@ def get_data_for_context_card(
|
|
|
396
390
|
TWENTY_NINETEEN: {METRIC_VALUE: twenty_nineteen_data},
|
|
397
391
|
}
|
|
398
392
|
|
|
399
|
-
if include_data_from_2_years_ago:
|
|
400
|
-
date_2_years_ago = get_a_previous_date(previous_year_date, "previous")
|
|
401
|
-
data_from_2_years_ago = get_latest_data_for_year(
|
|
402
|
-
df_measure,
|
|
403
|
-
date_2_years_ago,
|
|
404
|
-
value_column,
|
|
405
|
-
abbreviate_month,
|
|
406
|
-
data_expected_for_previous_year_and_previous_2years,
|
|
407
|
-
include_percentage_change,
|
|
408
|
-
previous_year_date,
|
|
409
|
-
)
|
|
410
|
-
data_to_return = {**data_to_return, PREVIOUS_2YEAR: data_from_2_years_ago}
|
|
411
|
-
|
|
412
393
|
return data_to_return
|
|
413
394
|
|
|
414
395
|
|
|
@@ -438,6 +419,9 @@ def get_a_previous_date(
|
|
|
438
419
|
return new_date
|
|
439
420
|
|
|
440
421
|
|
|
422
|
+
# if include_data_from_2_years_ago:
|
|
423
|
+
|
|
424
|
+
|
|
441
425
|
def get_latest_data_for_year(
|
|
442
426
|
df_measure: pl.DataFrame,
|
|
443
427
|
date: str,
|
|
@@ -544,3 +528,451 @@ def get_latest_data_for_year(
|
|
|
544
528
|
PERCENTAGE_CHANGE_FROM_TWO_PREV_YEAR
|
|
545
529
|
)[0]
|
|
546
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
|
|
@@ -64,6 +64,7 @@ class TimeSeriesChart:
|
|
|
64
64
|
filtered_df: pl.DataFrame,
|
|
65
65
|
trace_name_list: list[str],
|
|
66
66
|
dashed_trace_name_list: list[str] = None,
|
|
67
|
+
trace_colour_groups: list[str, str] = None,
|
|
67
68
|
initially_hidden_traces: Optional[list[str]] = None,
|
|
68
69
|
hover_data_for_traces_with_different_hover_for_last_point: Optional[
|
|
69
70
|
HoverDataByTrace
|
|
@@ -99,6 +100,7 @@ class TimeSeriesChart:
|
|
|
99
100
|
self.filtered_df = filtered_df
|
|
100
101
|
self.trace_name_list = trace_name_list
|
|
101
102
|
self.dashed_trace_name_list = dashed_trace_name_list
|
|
103
|
+
self.trace_colour_groups = trace_colour_groups
|
|
102
104
|
self.initially_hidden_traces = initially_hidden_traces
|
|
103
105
|
self.legend_dict = legend_dict
|
|
104
106
|
self.trace_name_column = trace_name_column
|
|
@@ -677,27 +679,97 @@ class TimeSeriesChart:
|
|
|
677
679
|
return df_list
|
|
678
680
|
|
|
679
681
|
def _get_colour_list(self):
|
|
680
|
-
"""Returns a list of colours.
|
|
682
|
+
"""Returns a list of colours (one per trace in trace_name_list).
|
|
683
|
+
|
|
684
|
+
If `trace_colour_groups` is provided, traces in the same group share a colour.
|
|
685
|
+
Traces not in any group get their own colour from the palette.
|
|
686
|
+
"""
|
|
687
|
+
palette = self._get_base_palette()
|
|
688
|
+
palette = self._apply_colour_shift(palette)
|
|
689
|
+
|
|
690
|
+
groups = getattr(self, "trace_colour_groups", None) or []
|
|
691
|
+
if not groups:
|
|
692
|
+
return palette[: len(self.trace_name_list)]
|
|
693
|
+
|
|
694
|
+
trace_to_group_id = self._build_trace_to_group_id(groups)
|
|
695
|
+
return self._assign_colours_with_groups(palette, trace_to_group_id)
|
|
696
|
+
|
|
697
|
+
def _get_base_palette(self) -> list[str]:
|
|
681
698
|
number_of_traces = len(self.trace_name_list)
|
|
682
699
|
if number_of_traces == 2 and self.filled_traces_dict is None:
|
|
683
|
-
|
|
700
|
+
return [
|
|
684
701
|
AFAccessibleColours.DARK_BLUE.value,
|
|
685
702
|
AFAccessibleColours.ORANGE.value,
|
|
686
|
-
]
|
|
687
|
-
|
|
688
|
-
|
|
703
|
+
]
|
|
704
|
+
return AFAccessibleColours.CATEGORICAL.value.copy()
|
|
705
|
+
|
|
706
|
+
def _apply_colour_shift(self, palette: list[str]) -> list[str]:
|
|
707
|
+
"""Apply number_of_traces_colour_shift_dict; may replace palette with explicit list."""
|
|
708
|
+
number_of_traces = len(self.trace_name_list)
|
|
689
709
|
colour_shift_dict = (
|
|
690
710
|
{"default": 0}
|
|
691
711
|
if self.number_of_traces_colour_shift_dict is None
|
|
692
712
|
else self.number_of_traces_colour_shift_dict
|
|
693
713
|
)
|
|
694
|
-
|
|
695
714
|
colour_shift_value = colour_shift_dict.get(
|
|
696
715
|
number_of_traces, colour_shift_dict["default"]
|
|
697
716
|
)
|
|
717
|
+
|
|
698
718
|
if isinstance(colour_shift_value, list):
|
|
699
|
-
return colour_shift_value
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
719
|
+
return colour_shift_value
|
|
720
|
+
|
|
721
|
+
# rotate left by colour_shift_value
|
|
722
|
+
shift = int(colour_shift_value)
|
|
723
|
+
if shift <= 0:
|
|
724
|
+
return palette
|
|
725
|
+
|
|
726
|
+
shift = shift % len(palette)
|
|
727
|
+
return palette[shift:] + palette[:shift]
|
|
728
|
+
|
|
729
|
+
def _build_trace_to_group_id(self, groups: list[list[str]]) -> dict[str, int]:
|
|
730
|
+
"""Validate groups and return a mapping of trace -> group_id."""
|
|
731
|
+
trace_set = set(self.trace_name_list)
|
|
732
|
+
trace_to_group_id: dict[str, int] = {}
|
|
733
|
+
|
|
734
|
+
for group_id, group in enumerate(groups):
|
|
735
|
+
for trace in group:
|
|
736
|
+
if trace not in trace_set:
|
|
737
|
+
raise ValueError(
|
|
738
|
+
f"trace_colour_groups contains '{trace}', but it's not in trace_name_list."
|
|
739
|
+
)
|
|
740
|
+
if trace in trace_to_group_id:
|
|
741
|
+
raise ValueError(
|
|
742
|
+
f"Trace '{trace}' appears in more than one trace_colour_group."
|
|
743
|
+
)
|
|
744
|
+
trace_to_group_id[trace] = group_id
|
|
745
|
+
|
|
746
|
+
return trace_to_group_id
|
|
747
|
+
|
|
748
|
+
def _assign_colours_with_groups(
|
|
749
|
+
self,
|
|
750
|
+
palette: list[str],
|
|
751
|
+
trace_to_group_id: dict[str, int],
|
|
752
|
+
) -> list[str]:
|
|
753
|
+
"""Assign one colour per group (first-seen), and one per ungrouped trace."""
|
|
754
|
+
group_colour: dict[int, str] = {}
|
|
755
|
+
trace_colour: dict[str, str] = {}
|
|
756
|
+
colour_idx = 0
|
|
757
|
+
|
|
758
|
+
# assign colours to groups in order of first appearance in trace_name_list
|
|
759
|
+
for trace in self.trace_name_list:
|
|
760
|
+
group_id = trace_to_group_id.get(trace)
|
|
761
|
+
if group_id is not None and group_id not in group_colour:
|
|
762
|
+
group_colour[group_id] = palette[colour_idx % len(palette)]
|
|
763
|
+
colour_idx += 1
|
|
764
|
+
|
|
765
|
+
# apply group colours to grouped traces
|
|
766
|
+
for trace, group_id in trace_to_group_id.items():
|
|
767
|
+
trace_colour[trace] = group_colour[group_id]
|
|
768
|
+
|
|
769
|
+
# assign colours to ungrouped traces
|
|
770
|
+
for trace in self.trace_name_list:
|
|
771
|
+
if trace not in trace_colour:
|
|
772
|
+
trace_colour[trace] = palette[colour_idx % len(palette)]
|
|
773
|
+
colour_idx += 1
|
|
774
|
+
|
|
775
|
+
return [trace_colour[t] for t in self.trace_name_list]
|