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.
Files changed (32) hide show
  1. gov_uk_dashboards/assets/dashboard.css +177 -0
  2. gov_uk_dashboards/assets/images/CHASE_icon.svg +17 -0
  3. gov_uk_dashboards/assets/images/explore_data_logo.svg +87 -0
  4. gov_uk_dashboards/colours.py +22 -0
  5. gov_uk_dashboards/components/dash/__init__.py +1 -1
  6. gov_uk_dashboards/components/dash/banners.py +20 -0
  7. gov_uk_dashboards/components/dash/context_card.py +472 -53
  8. gov_uk_dashboards/components/dash/data_quality_banner.py +91 -0
  9. gov_uk_dashboards/components/dash/download_button.py +0 -30
  10. gov_uk_dashboards/components/dash/footer.py +72 -29
  11. gov_uk_dashboards/components/dash/green_button.py +25 -0
  12. gov_uk_dashboards/components/dash/header.py +44 -0
  13. gov_uk_dashboards/components/dash/main_content.py +8 -1
  14. gov_uk_dashboards/components/dash/notification_banner.py +8 -5
  15. gov_uk_dashboards/components/leaflet/leaflet_choropleth_map.py +66 -27
  16. gov_uk_dashboards/components/plotly/enums.py +2 -0
  17. gov_uk_dashboards/components/plotly/stacked_barchart.py +53 -15
  18. gov_uk_dashboards/components/plotly/time_series_chart.py +140 -15
  19. gov_uk_dashboards/constants.py +19 -0
  20. gov_uk_dashboards/formatting/number_formatting.py +7 -0
  21. gov_uk_dashboards/lib/datetime_functions/datetime_functions.py +85 -0
  22. gov_uk_dashboards/lib/http_headers.py +3 -2
  23. gov_uk_dashboards/lib/testing_functions/data_test_assertions.py +3 -3
  24. gov_uk_dashboards/lib/testing_functions/data_test_helper_functions.py +2 -1
  25. gov_uk_dashboards/log_kpi.py +37 -0
  26. gov_uk_dashboards/template.html +1 -1
  27. gov_uk_dashboards/template.py +6 -0
  28. {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/METADATA +2 -2
  29. {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/RECORD +32 -27
  30. {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/WHEEL +1 -1
  31. {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/licenses/LICENSE +0 -0
  32. {gov_uk_dashboards-26.8.0.dist-info → gov_uk_dashboards-26.26.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,91 @@
1
+ """data_quality_banner"""
2
+
3
+ from enum import Enum
4
+ from dataclasses import dataclass
5
+ from dash import html
6
+ from gov_uk_dashboards.components.dash.notification_banner import notification_banner
7
+ from gov_uk_dashboards.formatting.text_functions import create_id_from_string
8
+ from gov_uk_dashboards.constants import (
9
+ NOTIFICATION_STYLE_GREEN,
10
+ NOTIFICATION_STYLE_ORANGE,
11
+ NOTIFICATION_STYLE_RED,
12
+ NOTIFICATION_STYLE_YELLOW,
13
+ )
14
+
15
+
16
+ @dataclass
17
+ class DataQualityConfig:
18
+ """
19
+ Configuration class for defining the display and linking details of a data quality metric.
20
+
21
+ Attributes:
22
+ title (str): The title of the data quality metric.
23
+ text (str): Descriptive text or explanation of the metric.
24
+ style (str): The visual style or format for displaying the metric.
25
+ title_color (str, optional): Optional color to use for the title. Defaults to None.
26
+ glossary_url (str, optional): Optional URL linking to a glossary entry for this metric.
27
+ If not provided, a URL is automatically generated based on the title.
28
+
29
+ Methods:
30
+ __post_init__(): Automatically generates a glossary URL if none is provided.
31
+ """
32
+
33
+ title: str
34
+ text: str
35
+ style: str
36
+ title_color: str = None
37
+ glossary_url: str = None
38
+
39
+ def __post_init__(self):
40
+ if not self.glossary_url:
41
+ slug = create_id_from_string(self.title)
42
+ self.glossary_url = f"/glossary#data-quality-{slug}"
43
+
44
+
45
+ class DataQualityLabels(Enum):
46
+ """Enumeration of standard labels used to categorize or describe data quality."""
47
+
48
+ OFFICIAL = DataQualityConfig(
49
+ title="OFFICIAL public data",
50
+ text="Use with confidence.",
51
+ style=NOTIFICATION_STYLE_GREEN,
52
+ )
53
+ MI = DataQualityConfig(
54
+ title="Management information",
55
+ text="Use for early insights, but with caution.",
56
+ style=NOTIFICATION_STYLE_YELLOW,
57
+ title_color="black",
58
+ )
59
+ EXPERIMENTAL_MI = DataQualityConfig(
60
+ title="Experimental management information",
61
+ text="Indicative only, requires expert guidance on use.",
62
+ style=NOTIFICATION_STYLE_ORANGE,
63
+ title_color="black",
64
+ )
65
+ OPERATIONAL = DataQualityConfig(
66
+ title="Operational data",
67
+ text="Never use in isolation, always verify independently.",
68
+ style=NOTIFICATION_STYLE_RED,
69
+ )
70
+
71
+
72
+ def data_quality_notification_banner(label: DataQualityLabels):
73
+ """Return data quality notification banner based on Gov UK Design component notification
74
+ banner component."""
75
+ config = label.value
76
+ text = [
77
+ f"{config.text} Read more in our ",
78
+ html.A(
79
+ "glossary",
80
+ href=config.glossary_url,
81
+ target="_blank",
82
+ rel="noopener noreferrer",
83
+ ),
84
+ ".",
85
+ ]
86
+ return notification_banner(
87
+ title=config.title,
88
+ text=text,
89
+ style=config.style,
90
+ title_color=config.title_color,
91
+ )
@@ -1,41 +1,11 @@
1
1
  """download_button"""
2
2
 
3
3
  from typing import Union
4
- import warnings
5
4
  from dash import html
6
5
 
7
6
  from gov_uk_dashboards.constants import DOWNLOAD_BUTTON_CLASSES
8
7
 
9
8
 
10
- def download_button(button_text: str, button_id: str = "download-button"):
11
- """
12
- Return a download button which is aligned to the right
13
-
14
- Args:
15
- button_text (str): The text to display on the button.
16
- button_id: (str, Optional) = id for dropdown, default to "download-button"
17
- """
18
- warnings.warn(
19
- "Note there is an alternative function to download_button() called "
20
- "create_download_button_with_icon() which includes a download icon and improved styling.",
21
- Warning,
22
- stacklevel=2,
23
- )
24
-
25
- return html.Div(
26
- [
27
- html.Button(
28
- button_text,
29
- id=button_id,
30
- n_clicks=0,
31
- className="govuk-button govuk-button--secondary",
32
- ),
33
- ],
34
- className="govuk-button-group",
35
- style={"float": "right"},
36
- )
37
-
38
-
39
9
  def create_download_button_with_icon(
40
10
  button_text: str,
41
11
  button_id_name: str,
@@ -4,7 +4,7 @@ from typing import Optional
4
4
  from dash import html
5
5
 
6
6
 
7
- def footer(footer_links: Optional[list[any]]):
7
+ def footer(footer_links: Optional[list[any]], include_logos: bool = False):
8
8
  """
9
9
  HTML component for a Gov.UK standard footer.
10
10
 
@@ -30,37 +30,80 @@ def footer(footer_links: Optional[list[any]]):
30
30
  if footer_links
31
31
  else None
32
32
  ),
33
- (
34
- html.Ul(
35
- children=[
36
- html.Li(
37
- item,
38
- className="govuk-footer__inline-list-item",
33
+ html.Div(
34
+ [
35
+ (
36
+ html.Ul(
37
+ children=[
38
+ html.Li(
39
+ item,
40
+ className="govuk-footer__inline-list-item",
41
+ )
42
+ for item in footer_links
43
+ ],
44
+ className=(
45
+ "govuk-footer__inline-list "
46
+ "govuk-!-display-none-print"
47
+ ),
39
48
  )
40
- for item in footer_links
41
- ],
42
- className=(
43
- "govuk-footer__inline-list "
44
- "govuk-!-display-none-print"
49
+ if footer_links
50
+ else None
45
51
  ),
46
- )
47
- if footer_links
48
- else None
49
- ),
50
- html.Span(
51
- [
52
- "All content is available under the ",
53
- html.A(
54
- "Open Government Licence v3.0",
55
- rel="license",
56
- href="https://www.nationalarchives.gov.uk/doc/"
57
- "open-government-licence/version/3/",
58
- className="govuk-footer__link",
59
- target="_blank",
52
+ html.Span(
53
+ [
54
+ "All content is available under the ",
55
+ html.A(
56
+ "Open Government Licence v3.0",
57
+ rel="license",
58
+ href="https://www.nationalarchives.gov.uk/doc/"
59
+ "open-government-licence/version/3/",
60
+ className="govuk-footer__link",
61
+ target="_blank",
62
+ ),
63
+ ", except where otherwise stated",
64
+ ],
65
+ className="govuk-footer__licence-description",
66
+ ),
67
+ (
68
+ (
69
+ html.Div(
70
+ [
71
+ html.Img(
72
+ src="assets\\images\\CHASE_icon.svg",
73
+ className="header-image",
74
+ style={"maxWidth": "100px"},
75
+ ),
76
+ html.Div(
77
+ [
78
+ html.B(
79
+ "CHASE",
80
+ style={
81
+ "font-size": "36px"
82
+ },
83
+ ),
84
+ ],
85
+ style={
86
+ "display": "flex",
87
+ "flex-direction": "column",
88
+ "align-items": "left",
89
+ "color": "#707070",
90
+ "maxWidth": "200px",
91
+ "padding-bottom": "10px",
92
+ },
93
+ ),
94
+ ],
95
+ style={
96
+ "display": "flex",
97
+ "flex-direction": "row",
98
+ "align-items": "center",
99
+ "padding-top": "30px",
100
+ },
101
+ )
102
+ )
103
+ if include_logos
104
+ else None
60
105
  ),
61
- ", except where otherwise stated",
62
- ],
63
- className="govuk-footer__licence-description",
106
+ ]
64
107
  ),
65
108
  ],
66
109
  className="govuk-footer__meta-item govuk-footer__meta-item--grow",
@@ -0,0 +1,25 @@
1
+ """green_button"""
2
+
3
+ from dash import html
4
+
5
+
6
+ def green_button(button_text: str, button_id: str):
7
+ """
8
+ Return a green button which is aligned to the right
9
+
10
+ Args:
11
+ button_text (str): The text to display on the button.
12
+ button_id: (str) = id for button
13
+ """
14
+
15
+ return html.Div(
16
+ [
17
+ html.Button(
18
+ button_text,
19
+ id=button_id,
20
+ n_clicks=0,
21
+ className="govuk-button",
22
+ ),
23
+ ],
24
+ className="govuk-button-group",
25
+ )
@@ -78,3 +78,47 @@ def header(
78
78
  | {"borderColor": "#000000", "backgroundColor": "rgb(0,98,94)"},
79
79
  **{"data-module": "govuk-header"},
80
80
  )
81
+
82
+
83
+ def logo_header():
84
+ """
85
+ Create and return the application header containing the Explore Data logo.
86
+
87
+ This component renders a GOV.UK–styled header with an SVG logo.
88
+ The logo is loaded from the Dash assets directory and constrained
89
+ to a maximum height for consistent layout.
90
+
91
+ Returns
92
+ -------
93
+ dash.html.Header
94
+ A Dash Header component containing the styled logo container.
95
+ """
96
+ return html.Header(
97
+ html.Div(
98
+ html.Div(
99
+ [
100
+ html.Div(
101
+ [
102
+ html.Img(
103
+ src="/assets/images/explore_data_logo.svg",
104
+ className="header-image",
105
+ style={"maxHeight": "100px"},
106
+ ),
107
+ ],
108
+ style={
109
+ "display": "flex",
110
+ "flexDirection": "column",
111
+ "color": "#00625e",
112
+ "alignItems": "center",
113
+ "paddingBottom": "2px",
114
+ },
115
+ ),
116
+ ],
117
+ className="govuk-header__content",
118
+ ),
119
+ style={"borderBottom": "0px solid #000000", "borderColor": "#f3f2f1"},
120
+ className="govuk-header__container govuk-width-container",
121
+ ),
122
+ className="govuk-header",
123
+ style={"backgroundColor": "#f3f2f1", "borderColor": "#f3f2f1"},
124
+ )
@@ -5,7 +5,7 @@ import re
5
5
  from dash import html
6
6
 
7
7
 
8
- def main_content(children, id_fragment=None):
8
+ def main_content(children, id_fragment=None, include_feedback_banner_div: bool = False):
9
9
  """
10
10
  Wrapper for the main content of the dashboard, containing visualisations.
11
11
 
@@ -29,4 +29,11 @@ def main_content(children, id_fragment=None):
29
29
  UserWarning,
30
30
  stacklevel=2,
31
31
  )
32
+
33
+ if not isinstance(children, list):
34
+ children = [children]
35
+
36
+ if include_feedback_banner_div:
37
+ children.append(html.Div(id="feedback-banner"))
38
+
32
39
  return html.Main(children, className="main", id=slug, role="main")
@@ -4,7 +4,11 @@ from dash import html
4
4
 
5
5
 
6
6
  def notification_banner(
7
- text: str, text_class_name: str = "govuk-warning-text__text", style: dict = None
7
+ text: list,
8
+ title: str = "Important",
9
+ text_class_name: str = "govuk-notification-banner__heading",
10
+ style: dict = None,
11
+ title_color: str = None,
8
12
  ):
9
13
  """
10
14
  Return Gov UK Design component notification banner component.
@@ -14,9 +18,10 @@ def notification_banner(
14
18
  html.Div(
15
19
  [
16
20
  html.H2(
17
- ["Important"],
21
+ [title],
18
22
  className="govuk-notification-banner__title",
19
23
  id="govuk-notification-banner-title",
24
+ style={"color": title_color} if title_color else None,
20
25
  )
21
26
  ],
22
27
  className="govuk-notification-banner__header",
@@ -24,9 +29,7 @@ def notification_banner(
24
29
  html.Div(
25
30
  [
26
31
  html.P(
27
- [
28
- text,
29
- ],
32
+ text,
30
33
  className=text_class_name,
31
34
  )
32
35
  ],
@@ -42,8 +42,9 @@ class LeafletChoroplethMap:
42
42
  download_chart_button_id: Optional[str] = None,
43
43
  download_data_button_id: Optional[str] = None,
44
44
  color_scale_is_discrete: bool = True,
45
+ colorbar_title: str = None,
45
46
  show_tile_layer: bool = False,
46
- ):
47
+ ): # pylint: disable=too-many-locals
47
48
  self.geojson_data = geojson
48
49
  self.df = df
49
50
  self.hover_text_columns = hover_text_columns
@@ -56,6 +57,7 @@ class LeafletChoroplethMap:
56
57
  self.download_chart_button_id = download_chart_button_id
57
58
  self.download_data_button_id = download_data_button_id
58
59
  self.color_scale_is_discrete = color_scale_is_discrete
60
+ self.colorbar_title = self.resolve_colorbar_title(colorbar_title)
59
61
  self.show_tile_layer = show_tile_layer
60
62
  self._add_data_to_geojson()
61
63
  self.instance_number = instance_number
@@ -198,11 +200,28 @@ class LeafletChoroplethMap:
198
200
 
199
201
  def _get_colorscale(self):
200
202
  if self.color_scale_is_discrete:
201
- discrete_colours = ["#217847", "#23BBBE", "#8CCE69", "#FFEA80"]
202
203
  if len(self.df[self.column_to_plot].unique()) == 3:
203
- discrete_colours.pop(1)
204
- return discrete_colours
205
- return ["#B0F2BC", "#257D98"]
204
+ return ["#080C54", "#1F9EB7", "#CDE594"]
205
+ if len(self.df[self.column_to_plot].unique()) == 4:
206
+ return ["#080C54", "#1F9EB7", "#80C6A3", "#CDE594"]
207
+ if len(self.df[self.column_to_plot].unique()) == 5:
208
+ return [
209
+ "#080C54",
210
+ "#186290",
211
+ "#1F9EB7",
212
+ "#80C6A3",
213
+ "#CDE594",
214
+ ]
215
+ if len(self.df[self.column_to_plot].unique()) == 6:
216
+ return [
217
+ "#080C54",
218
+ "#186290",
219
+ "#1F9EB7",
220
+ "#80C6A3",
221
+ "#CDE594",
222
+ "#ffffcc",
223
+ ]
224
+ return ["#80C6A3", "#186290"]
206
225
 
207
226
  def _get_color_bar_categories(self):
208
227
  return (
@@ -215,10 +234,10 @@ class LeafletChoroplethMap:
215
234
  )
216
235
 
217
236
  def _get_colorbar(self):
237
+ top_margin = ("100px" if self.colorbar_title else None,)
218
238
  if self.color_scale_is_discrete:
219
- self._get_color_bar_categories()
220
239
  return dlx.categorical_colorbar(
221
- categories=self._get_color_bar_categories(), # reversed order
240
+ categories=self._get_color_bar_categories(),
222
241
  colorscale=self._get_colorscale()[::-1],
223
242
  width=30,
224
243
  height=200,
@@ -228,8 +247,10 @@ class LeafletChoroplethMap:
228
247
  "backgroundColor": "white",
229
248
  "borderRadius": "4px",
230
249
  "fontSize": "16px",
250
+ "marginTop": top_margin,
231
251
  },
232
252
  )
253
+
233
254
  min_value = self.df.select(pl.min(self.column_to_plot)).item()
234
255
  colorbar_min = min(min_value, 0)
235
256
  max_value = self.df.select(pl.max(self.column_to_plot)).item()
@@ -243,9 +264,19 @@ class LeafletChoroplethMap:
243
264
  three_quarter_value,
244
265
  max_value,
245
266
  ]
267
+
268
+ tick_text = [format_number_into_thousands_or_millions(x) for x in tick_values]
269
+
270
+ # If duplicates appear in tick_text, change rounding
271
+ if len(set(tick_text)) < len(tick_text):
272
+ tick_text = [
273
+ format_number_into_thousands_or_millions(x, 1) for x in tick_values
274
+ ]
275
+
246
276
  tick_text = [
247
- format_number_into_thousands_or_millions(x) for x in tick_values
248
- ] # Optional, for formatting
277
+ str(int(val)) if val < 1000 else text
278
+ for text, val in zip(tick_text, tick_values)
279
+ ] # values less than 1000 are ints
249
280
 
250
281
  return dl.Colorbar(
251
282
  colorscale=self._get_colorscale(),
@@ -258,27 +289,35 @@ class LeafletChoroplethMap:
258
289
  "backgroundColor": "white",
259
290
  "padding": "5px",
260
291
  "borderRadius": "4px",
261
- "marginTop": "100px",
292
+ "marginTop": top_margin,
262
293
  },
263
294
  tickValues=tick_values,
264
295
  tickText=tick_text, # Optional, makes labels look cleaner
265
296
  )
266
297
 
267
298
  def _get_colorbar_title(self, enable_zoom: bool = False):
268
- if self.color_scale_is_discrete:
269
- return None
270
- top = "70px" if enable_zoom is False else "140px"
271
- return html.Div(
272
- self.hover_text_columns[0],
273
- style={
274
- "position": "absolute",
275
- "top": top, # Adjusted to place above the colorbar
276
- "left": "10px", # Align with the left side of the colorbar
277
- "background": "white",
278
- "padding": "2px 6px",
279
- "borderRadius": "5px",
280
- "fontWeight": "bold",
281
- "fontSize": "14px",
282
- "zIndex": "999", # Ensure it appears above map elements
283
- },
284
- )
299
+ if self.colorbar_title:
300
+ top = "70px" if enable_zoom is False else "140px"
301
+ return html.Div(
302
+ self.colorbar_title,
303
+ style={
304
+ "position": "absolute",
305
+ "top": top, # Adjusted to place above the colorbar
306
+ "left": "10px", # Align with the left side of the colorbar
307
+ "background": "white",
308
+ "padding": "2px 6px",
309
+ "borderRadius": "5px",
310
+ "fontWeight": "bold",
311
+ "fontSize": "14px",
312
+ "zIndex": "999", # Ensure it appears above map elements
313
+ },
314
+ )
315
+ return None
316
+
317
+ def resolve_colorbar_title(self, colorbar_title: str):
318
+ """Returns text for colorbar title."""
319
+ if colorbar_title is None:
320
+ return None # exclude title
321
+ if colorbar_title == "default":
322
+ return self.hover_text_columns[0]
323
+ return colorbar_title # custom title
@@ -11,6 +11,8 @@ class XAxisFormat(Enum):
11
11
  MONTH_YEAR = "month_year"
12
12
  MONTH_YEAR_MONTHLY_DATA = "month_year_monthly_data"
13
13
  FINANCIAL_QUARTER = "financial_quarter"
14
+ WEEK = "week"
15
+ QUARTER = "quarter"
14
16
 
15
17
 
16
18
  class TitleDataStructure(TypedDict):