gov-uk-dashboards 16.2.0__py3-none-any.whl → 17.1.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/custom_map_style_functions.js +44 -0
- gov_uk_dashboards/components/dash/__init__.py +0 -1
- gov_uk_dashboards/components/dash/download_button.py +28 -24
- gov_uk_dashboards/components/helpers/display_chart_or_table_with_header.py +17 -22
- gov_uk_dashboards/components/helpers/get_chart_for_download.py +21 -0
- gov_uk_dashboards/components/leaflet/__init__.py +0 -0
- gov_uk_dashboards/components/leaflet/leaflet_choropleth_map.py +174 -0
- gov_uk_dashboards/components/plotly/stacked_barchart.py +7 -0
- gov_uk_dashboards/components/plotly/time_series_chart.py +7 -0
- gov_uk_dashboards/constants.py +6 -0
- gov_uk_dashboards/formatting/number_formatting.py +24 -0
- gov_uk_dashboards/lib/download_functions/__init__.py +0 -0
- gov_uk_dashboards/lib/download_functions/convert_fig_to_image_and_download.py +16 -0
- gov_uk_dashboards/lib/download_functions/download_csv_with_headers.py +114 -0
- {gov_uk_dashboards-16.2.0.dist-info → gov_uk_dashboards-17.1.0.dist-info}/METADATA +1 -1
- {gov_uk_dashboards-16.2.0.dist-info → gov_uk_dashboards-17.1.0.dist-info}/RECORD +19 -13
- {gov_uk_dashboards-16.2.0.dist-info → gov_uk_dashboards-17.1.0.dist-info}/WHEEL +1 -1
- gov_uk_dashboards/components/dash/create_download_chart_button.py +0 -25
- gov_uk_dashboards/components/dash/create_download_data_button.py +0 -25
- {gov_uk_dashboards-16.2.0.dist-info → gov_uk_dashboards-17.1.0.dist-info}/licenses/LICENSE +0 -0
- {gov_uk_dashboards-16.2.0.dist-info → gov_uk_dashboards-17.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
window.myNamespace = Object.assign({}, window.myNamespace, {
|
2
|
+
mapColorScaleFunctions: {
|
3
|
+
continuousColorScale: function(feature, context) {
|
4
|
+
const {
|
5
|
+
colorscale,
|
6
|
+
colorProp,
|
7
|
+
style,
|
8
|
+
min,
|
9
|
+
max
|
10
|
+
} = context.hideout;
|
11
|
+
const value = feature.properties[colorProp];
|
12
|
+
const colors = Array.from(colorscale); // defensive copy
|
13
|
+
if (value === null || value === undefined) {
|
14
|
+
return {
|
15
|
+
...style,
|
16
|
+
fillColor: "#b1b4b6"
|
17
|
+
};
|
18
|
+
}
|
19
|
+
// Normalize value to 0-1
|
20
|
+
const t = (value - min) / (max - min);
|
21
|
+
|
22
|
+
// Helper: interpolate between two hex colors
|
23
|
+
function interpolateColor(color1, color2, t) {
|
24
|
+
const c1 = parseInt(color1.slice(1), 16);
|
25
|
+
const c2 = parseInt(color2.slice(1), 16);
|
26
|
+
const r = Math.round(((c2 >> 16) - (c1 >> 16)) * t + (c1 >> 16));
|
27
|
+
const g = Math.round((((c2 >> 8) & 0xFF) - ((c1 >> 8) & 0xFF)) * t + ((c1 >> 8) & 0xFF));
|
28
|
+
const b = Math.round(((c2 & 0xFF) - (c1 & 0xFF)) * t + (c1 & 0xFF));
|
29
|
+
return `rgb(${r},${g},${b})`;
|
30
|
+
}
|
31
|
+
|
32
|
+
// Find segment and interpolate
|
33
|
+
const n = colors.length - 1;
|
34
|
+
const idx = Math.min(Math.floor(t * n), n - 1);
|
35
|
+
const local_t = (t * n) - idx;
|
36
|
+
const fillColor = interpolateColor(colors[idx], colors[idx + 1], local_t);
|
37
|
+
|
38
|
+
return {
|
39
|
+
...style,
|
40
|
+
fillColor: fillColor
|
41
|
+
};
|
42
|
+
}
|
43
|
+
}
|
44
|
+
});
|
@@ -68,7 +68,6 @@ from .card import card, empty_card
|
|
68
68
|
from .card_full_width import card_full_width
|
69
69
|
from .apply_and_reset_filters_buttons import apply_and_reset_filters_buttons
|
70
70
|
from .download_button import download_button
|
71
|
-
from .download_button import download_button_with_icon
|
72
71
|
from .collapsible_panel import collapsible_panel
|
73
72
|
from .dashboard_container import dashboard_container
|
74
73
|
from .details import details
|
@@ -2,6 +2,8 @@
|
|
2
2
|
import warnings
|
3
3
|
from dash import html
|
4
4
|
|
5
|
+
from gov_uk_dashboards.constants import DOWNLOAD_BUTTON_CLASSES
|
6
|
+
|
5
7
|
|
6
8
|
def download_button(button_text: str, button_id: str = "download-button"):
|
7
9
|
"""
|
@@ -13,7 +15,7 @@ def download_button(button_text: str, button_id: str = "download-button"):
|
|
13
15
|
"""
|
14
16
|
warnings.warn(
|
15
17
|
"Note there is an alternative function to download_button() called "
|
16
|
-
"
|
18
|
+
"create_download_button_with_icon() which includes a download icon and improved styling.",
|
17
19
|
Warning,
|
18
20
|
stacklevel=2,
|
19
21
|
)
|
@@ -32,31 +34,33 @@ def download_button(button_text: str, button_id: str = "download-button"):
|
|
32
34
|
)
|
33
35
|
|
34
36
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
aligned to the
|
37
|
+
def create_download_button_with_icon(
|
38
|
+
button_text: str, button_id_name: str
|
39
|
+
) -> html.Button:
|
40
|
+
"""Create a download button with icon, aligned to the left.
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
Parameters:
|
43
|
+
- button_id_name (str): A unique identifier for the button.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
- html.Button: Download button.
|
43
47
|
"""
|
44
|
-
|
45
|
-
|
46
|
-
"govuk-!-margin-bottom-0 govuk-!-margin-top-4 "
|
47
|
-
"flex w-auto items-center gap-2 print:hidden"
|
48
|
-
)
|
49
|
-
return html.Div(
|
48
|
+
download_type = button_text.lower().replace(" ", "-")
|
49
|
+
return html.Button(
|
50
50
|
[
|
51
|
-
html.
|
52
|
-
|
53
|
-
id=button_id,
|
54
|
-
n_clicks=0,
|
55
|
-
className=classes,
|
56
|
-
type="submit",
|
57
|
-
style={"display": "flex", "alignItems": "center", "gap": "8px"},
|
58
|
-
),
|
51
|
+
html.Div("", className="download-icon"),
|
52
|
+
button_text,
|
59
53
|
],
|
60
|
-
|
61
|
-
|
54
|
+
id={
|
55
|
+
"download-type": download_type,
|
56
|
+
"name": button_id_name,
|
57
|
+
},
|
58
|
+
n_clicks=0,
|
59
|
+
className=DOWNLOAD_BUTTON_CLASSES,
|
60
|
+
type="submit",
|
61
|
+
style={
|
62
|
+
"display": "flex",
|
63
|
+
"alignItems": "center",
|
64
|
+
"gap": "8px",
|
65
|
+
},
|
62
66
|
)
|
@@ -3,11 +3,8 @@
|
|
3
3
|
from dash import html
|
4
4
|
from dash.development.base_component import Component
|
5
5
|
|
6
|
-
from gov_uk_dashboards.components.dash.
|
7
|
-
|
8
|
-
)
|
9
|
-
from gov_uk_dashboards.components.dash.create_download_data_button import (
|
10
|
-
create_download_data_button,
|
6
|
+
from gov_uk_dashboards.components.dash.download_button import (
|
7
|
+
create_download_button_with_icon,
|
11
8
|
)
|
12
9
|
from gov_uk_dashboards.components.dash.heading import HeadingSizes
|
13
10
|
|
@@ -36,11 +33,6 @@ def display_chart_or_table_with_header(
|
|
36
33
|
[
|
37
34
|
html.Div(
|
38
35
|
[
|
39
|
-
(
|
40
|
-
create_download_chart_button(button_id_name=download_button_id)
|
41
|
-
if download_button_id
|
42
|
-
else None
|
43
|
-
),
|
44
36
|
html.Div(
|
45
37
|
[
|
46
38
|
html.H2(
|
@@ -61,20 +53,23 @@ def display_chart_or_table_with_header(
|
|
61
53
|
html.Div(
|
62
54
|
(
|
63
55
|
[
|
64
|
-
html.
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
56
|
+
html.Div(
|
57
|
+
[
|
58
|
+
create_download_button_with_icon(
|
59
|
+
"Download chart", download_button_id
|
60
|
+
)
|
61
|
+
if download_button_id
|
62
|
+
else [],
|
63
|
+
create_download_button_with_icon(
|
64
|
+
"Download data", download_data_button_id
|
65
|
+
)
|
66
|
+
if download_data_button_id
|
67
|
+
else [],
|
68
|
+
],
|
69
|
+
className="govuk-button-group",
|
70
|
+
style={"padding-top": "20px"},
|
74
71
|
),
|
75
72
|
]
|
76
|
-
if download_data_button_id
|
77
|
-
else []
|
78
73
|
),
|
79
74
|
),
|
80
75
|
],
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""get_chart_for_download"""
|
2
|
+
from gov_uk_dashboards.constants import MAIN_TITLE, SUBTITLE
|
3
|
+
|
4
|
+
|
5
|
+
def get_chart_for_download(self, fig):
|
6
|
+
"""Returns a fig with title and subtitle for download as png"""
|
7
|
+
main_title = self.title_data[MAIN_TITLE]
|
8
|
+
subtitle = self.title_data[SUBTITLE]
|
9
|
+
|
10
|
+
fig.update_layout(
|
11
|
+
title={
|
12
|
+
"text": f"<b><span style='color: black; margin: 0px 0px 5px'>{main_title}</span></b>",
|
13
|
+
"x": 0.01,
|
14
|
+
"xanchor": "left",
|
15
|
+
},
|
16
|
+
title_subtitle={
|
17
|
+
"text": f"<b><span style='color: black; margin: 0px 0px 5px'>{subtitle}</span></b>"
|
18
|
+
},
|
19
|
+
margin={"t": 100},
|
20
|
+
)
|
21
|
+
return fig
|
File without changes
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"""Leaflet choropleth map class"""
|
2
|
+
|
3
|
+
from dash_extensions.javascript import arrow_function, Namespace
|
4
|
+
import dash_leaflet as dl
|
5
|
+
from dash import html
|
6
|
+
import polars as pl
|
7
|
+
|
8
|
+
from gov_uk_dashboards.formatting.number_formatting import (
|
9
|
+
format_number_into_thousands_or_millions,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class LeafletChoroplethMap:
|
14
|
+
"""Class for generating leaflet choropleth map charts.
|
15
|
+
Note: dataframe_function must contain columns: 'Region', 'Area_Code',
|
16
|
+
column_to_plot, hover_text_columns
|
17
|
+
If color_scale_is_discrete is false, colour scale will be continuous, otherwise it will be
|
18
|
+
discrete"""
|
19
|
+
|
20
|
+
# pylint: disable=too-few-public-methods
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
get_geojson_function,
|
25
|
+
get_df_function,
|
26
|
+
hover_text_columns,
|
27
|
+
color_scale_is_discrete=True,
|
28
|
+
):
|
29
|
+
self.geojson_data = get_geojson_function()
|
30
|
+
self.df = get_df_function()
|
31
|
+
self.hover_text_columns = hover_text_columns
|
32
|
+
self.color_scale_is_discrete = color_scale_is_discrete
|
33
|
+
self._add_data_to_geojson()
|
34
|
+
|
35
|
+
def get_leaflet_choropleth_map(self):
|
36
|
+
"""Creates and returns leaflet choropleth map chart for display on application.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
dl.Map: A dash leaflet map chart.
|
40
|
+
"""
|
41
|
+
return dl.Map(
|
42
|
+
children=[
|
43
|
+
dl.TileLayer(),
|
44
|
+
self._get_colorbar(),
|
45
|
+
self._get_colorbar_title(),
|
46
|
+
self._get_dl_geojson(),
|
47
|
+
],
|
48
|
+
center=[54.5, -2.5], # Centered on the UK
|
49
|
+
zoom=6.5,
|
50
|
+
minZoom=6.5,
|
51
|
+
maxZoom=6.5,
|
52
|
+
maxBounds=[[49.8, -10], [55.9, 1.8]],
|
53
|
+
scrollWheelZoom=False, # Disable zooming via mouse scroll
|
54
|
+
dragging=False, # Optional: prevent dragging too if you want
|
55
|
+
zoomControl=False, # Hide the zoom buttons (+/-)
|
56
|
+
doubleClickZoom=False, # Prevent double click zoom
|
57
|
+
touchZoom=False, # Prevent pinch zoom
|
58
|
+
attributionControl=False,
|
59
|
+
style={"width": "100%", "height": "800px", "background": "white"},
|
60
|
+
)
|
61
|
+
|
62
|
+
def _add_data_to_geojson(self):
|
63
|
+
info_map = {
|
64
|
+
row["Area_Code"]: {
|
65
|
+
"value": row["Value"],
|
66
|
+
"region": row["Region"],
|
67
|
+
**{col: row[col] for col in self.hover_text_columns},
|
68
|
+
}
|
69
|
+
for row in self.df.iter_rows(named=True)
|
70
|
+
}
|
71
|
+
|
72
|
+
for feature in self.geojson_data["features"]:
|
73
|
+
region_code = feature["properties"]["geo_id"]
|
74
|
+
info = info_map.get(region_code)
|
75
|
+
if info:
|
76
|
+
|
77
|
+
feature["properties"]["density"] = info["value"]
|
78
|
+
feature["properties"]["region"] = info["region"]
|
79
|
+
|
80
|
+
tooltip_parts = [f"<b>{info['region']}</b>"]
|
81
|
+
if info["value"] is None:
|
82
|
+
tooltip_parts.append("<br>No data available")
|
83
|
+
else:
|
84
|
+
for col in self.hover_text_columns:
|
85
|
+
tooltip_parts.append(f"<br>{col}: {info[col]}")
|
86
|
+
|
87
|
+
feature["properties"]["tooltip"] = "".join(tooltip_parts)
|
88
|
+
else:
|
89
|
+
feature["properties"]["density"] = None
|
90
|
+
feature["properties"]["region"] = "Unknown"
|
91
|
+
feature["properties"]["tooltip"] = "No data available"
|
92
|
+
|
93
|
+
def _get_dl_geojson(self):
|
94
|
+
style_handle = self._get_style_handle()
|
95
|
+
colorscale = self._get_colorscale()
|
96
|
+
style = {"weight": 2, "opacity": 1, "color": "white", "fillOpacity": 1}
|
97
|
+
hover_style = arrow_function({"weight": 5, "color": "#666", "dashArray": ""})
|
98
|
+
return dl.GeoJSON(
|
99
|
+
data=self.geojson_data,
|
100
|
+
id="geojson",
|
101
|
+
zoomToBounds=True,
|
102
|
+
zoomToBoundsOnClick=True,
|
103
|
+
style=style_handle,
|
104
|
+
hoverStyle=hover_style,
|
105
|
+
hideout={
|
106
|
+
"colorscale": colorscale, # Use hex strings
|
107
|
+
"style": style,
|
108
|
+
"colorProp": "density",
|
109
|
+
"min": self.df["Value"].min(),
|
110
|
+
"max": self.df["Value"].max(),
|
111
|
+
},
|
112
|
+
)
|
113
|
+
|
114
|
+
def _get_style_handle(self):
|
115
|
+
ns = Namespace("myNamespace", "mapColorScaleFunctions")
|
116
|
+
if self.color_scale_is_discrete:
|
117
|
+
return ""
|
118
|
+
return ns("continuousColorScale")
|
119
|
+
|
120
|
+
def _get_colorscale(self):
|
121
|
+
if self.color_scale_is_discrete:
|
122
|
+
return ""
|
123
|
+
return ["#B0F2BC", "#257D98"]
|
124
|
+
|
125
|
+
def _get_colorbar(self):
|
126
|
+
min_value = self.df.select(pl.min("Value")).item()
|
127
|
+
colorbar_min = min(min_value, 0)
|
128
|
+
max_value = self.df.select(pl.max("Value")).item()
|
129
|
+
mid_value = (colorbar_min + max_value) / 2
|
130
|
+
quarter_value = (colorbar_min + max_value) / 4
|
131
|
+
three_quarter_value = 3 * (colorbar_min + max_value) / 4
|
132
|
+
tick_values = [
|
133
|
+
colorbar_min,
|
134
|
+
quarter_value,
|
135
|
+
mid_value,
|
136
|
+
three_quarter_value,
|
137
|
+
max_value,
|
138
|
+
]
|
139
|
+
tick_text = [
|
140
|
+
format_number_into_thousands_or_millions(x) for x in tick_values
|
141
|
+
] # Optional, for formatting
|
142
|
+
|
143
|
+
return dl.Colorbar(
|
144
|
+
colorscale=self._get_colorscale(),
|
145
|
+
width=20,
|
146
|
+
height=200,
|
147
|
+
min=colorbar_min,
|
148
|
+
max=max_value,
|
149
|
+
position="topleft",
|
150
|
+
style={
|
151
|
+
"backgroundColor": "white",
|
152
|
+
"padding": "5px",
|
153
|
+
"borderRadius": "4px",
|
154
|
+
"marginTop": "100px",
|
155
|
+
},
|
156
|
+
tickValues=tick_values,
|
157
|
+
tickText=tick_text, # Optional, makes labels look cleaner
|
158
|
+
)
|
159
|
+
|
160
|
+
def _get_colorbar_title(self):
|
161
|
+
return html.Div(
|
162
|
+
self.hover_text_columns[0],
|
163
|
+
style={
|
164
|
+
"position": "absolute",
|
165
|
+
"bottom": "700px", # Adjusted to place above the colorbar
|
166
|
+
"left": "10px", # Align with the left side of the colorbar
|
167
|
+
"background": "white",
|
168
|
+
"padding": "2px 6px",
|
169
|
+
"borderRadius": "5px",
|
170
|
+
"fontWeight": "bold",
|
171
|
+
"fontSize": "14px",
|
172
|
+
"zIndex": "999", # Ensure it appears above map elements
|
173
|
+
},
|
174
|
+
)
|
@@ -7,6 +7,9 @@ import polars as pl
|
|
7
7
|
|
8
8
|
import plotly.graph_objects as go
|
9
9
|
|
10
|
+
from gov_uk_dashboards.components.helpers.get_chart_for_download import (
|
11
|
+
get_chart_for_download,
|
12
|
+
)
|
10
13
|
from gov_uk_dashboards.constants import (
|
11
14
|
CHART_LABEL_FONT_SIZE,
|
12
15
|
CUSTOM_DATA,
|
@@ -117,6 +120,10 @@ class StackedBarChart:
|
|
117
120
|
self.download_data_button_id,
|
118
121
|
)
|
119
122
|
|
123
|
+
def get_stacked_bar_chart_for_download(self):
|
124
|
+
"""Return fig with title and subtitle for download as png"""
|
125
|
+
return get_chart_for_download(self, self.create_stacked_bar_chart())
|
126
|
+
|
120
127
|
def create_stacked_bar_chart(
|
121
128
|
self,
|
122
129
|
):
|
@@ -30,6 +30,9 @@ from gov_uk_dashboards.components.helpers.plotting_helper_functions import (
|
|
30
30
|
get_legend_configuration,
|
31
31
|
get_rgba_from_hex_colour_and_alpha,
|
32
32
|
)
|
33
|
+
from gov_uk_dashboards.components.helpers.get_chart_for_download import (
|
34
|
+
get_chart_for_download,
|
35
|
+
)
|
33
36
|
from gov_uk_dashboards.components.helpers.update_layout_bgcolor_margin import (
|
34
37
|
update_layout_bgcolor_margin,
|
35
38
|
)
|
@@ -129,6 +132,10 @@ class TimeSeriesChart:
|
|
129
132
|
self.download_data_button_id,
|
130
133
|
)
|
131
134
|
|
135
|
+
def get_time_series_chart_for_download(self):
|
136
|
+
"""Return fig with title and subtitle for download as png"""
|
137
|
+
return get_chart_for_download(self, self.create_time_series_chart())
|
138
|
+
|
132
139
|
def create_time_series_chart(
|
133
140
|
self,
|
134
141
|
):
|
gov_uk_dashboards/constants.py
CHANGED
@@ -18,3 +18,9 @@ UNIT_SIZE = "Unit size"
|
|
18
18
|
DEFAULT_COLOURSCALE = "tealgrn"
|
19
19
|
|
20
20
|
LEGEND_SPACING = "\u00A0" * 5
|
21
|
+
|
22
|
+
DOWNLOAD_BUTTON_CLASSES = (
|
23
|
+
"govuk-button govuk-button--primary "
|
24
|
+
"govuk-!-margin-bottom-0 govuk-!-margin-top-4 "
|
25
|
+
"flex w-auto items-center gap-2 print:hidden"
|
26
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
"""Functions to add formatting to number values"""
|
2
|
+
|
3
|
+
|
4
|
+
def format_number_into_thousands_or_millions(
|
5
|
+
number: int, thousand_decimal_places: int = 0
|
6
|
+
) -> str:
|
7
|
+
"""Format number into thousands or millions, eg. 1,500 becomes 1.5k & 1,234,567 becomes 1.235m
|
8
|
+
|
9
|
+
Args:
|
10
|
+
number (int): Integer to format.
|
11
|
+
thousand_decimal_places (int or str): The number of decimal places to display for
|
12
|
+
1_000<=number<1_000_000. Defaults to 0. If "default" is passed, number is simply
|
13
|
+
divided by 1_000.
|
14
|
+
"""
|
15
|
+
if number >= 1_000_000:
|
16
|
+
formatted_number = f"{number / 1_000_000:.3f}m"
|
17
|
+
elif number >= 1_000:
|
18
|
+
if thousand_decimal_places == "default":
|
19
|
+
formatted_number = f"{number / 1_000}k"
|
20
|
+
else:
|
21
|
+
formatted_number = f"{number / 1_000:.{thousand_decimal_places}f}k"
|
22
|
+
else:
|
23
|
+
formatted_number = str(number)
|
24
|
+
return formatted_number
|
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"""convert_fig_to_image_and_download"""
|
2
|
+
|
3
|
+
import plotly.graph_objects as go
|
4
|
+
import plotly.io as pio
|
5
|
+
from dash import dcc
|
6
|
+
|
7
|
+
|
8
|
+
def convert_fig_to_image_and_download(fig: go.Figure, name: str):
|
9
|
+
"""Converts a given Plotly figure to a PNG image and returns a Dash component
|
10
|
+
for downloading the image.
|
11
|
+
"""
|
12
|
+
fig.update_layout(margin={"t": 150, "b": 100, "l": 100, "r": 50})
|
13
|
+
|
14
|
+
filename = f"{name}-chart.png"
|
15
|
+
img_bytes = pio.to_image(fig, format="png", width=1600, height=800)
|
16
|
+
return dcc.send_bytes(img_bytes, filename)
|
@@ -0,0 +1,114 @@
|
|
1
|
+
"""download_csv_with_headers"""
|
2
|
+
import io
|
3
|
+
import polars as pl
|
4
|
+
from dash import dcc
|
5
|
+
from gov_uk_dashboards.lib.datetime_functions.datetime_functions import (
|
6
|
+
get_todays_date_for_downloaded_csv,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
def download_csv_with_headers(
|
11
|
+
list_of_df_title_subtitle_dicts: list[dict[str, str]],
|
12
|
+
name: str,
|
13
|
+
sensitivity_label: str,
|
14
|
+
additional_text: list[str] = None,
|
15
|
+
): # pylint: disable=too-many-locals
|
16
|
+
"""Adds a header above multiple dataframes,
|
17
|
+
separates them with blank rows, and downloads as CSV.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
list_of_df_title_subtitle_dicts (list[dict[]]): List of dictionaries containing keys: "df",
|
21
|
+
"title" and "subtitle"
|
22
|
+
name (str): Filename for CSV.
|
23
|
+
sensitivity_label (str): Sensitivity label. Str or None.
|
24
|
+
additional_text (list[str]): Additional text to inlcude in headers after data downloaded.
|
25
|
+
Str or None.
|
26
|
+
"""
|
27
|
+
|
28
|
+
csv_buffer = io.StringIO()
|
29
|
+
|
30
|
+
column_list = list(list_of_df_title_subtitle_dicts[0]["df"].columns)
|
31
|
+
column_dict = {column_name: column_name for column_name in column_list}
|
32
|
+
blank_dict = {
|
33
|
+
f"{i}": None
|
34
|
+
for i in range(
|
35
|
+
_get_number_of_max_columns_from_all_dfs(list_of_df_title_subtitle_dicts)
|
36
|
+
- len(column_list)
|
37
|
+
)
|
38
|
+
} # range is missing columns in first df compared to max columns across all dfs
|
39
|
+
|
40
|
+
subtitle = list_of_df_title_subtitle_dicts[0]["subtitle"]
|
41
|
+
|
42
|
+
header_data = [
|
43
|
+
{column_list[0]: "Date downloaded: " + get_todays_date_for_downloaded_csv()},
|
44
|
+
*(
|
45
|
+
[{column_list[0]: text} for text in additional_text]
|
46
|
+
+ [{column_list[0]: None}]
|
47
|
+
if additional_text is not None
|
48
|
+
else []
|
49
|
+
),
|
50
|
+
{column_list[0]: list_of_df_title_subtitle_dicts[0]["title"]},
|
51
|
+
*(
|
52
|
+
[{column_list[0]: subtitle}] if subtitle is not None else []
|
53
|
+
), # Uses unpacking (*) to add the subtitle row if subtitle is not None. If subtitle is
|
54
|
+
# None, it unpacks an empty list, effectively skipping the row.
|
55
|
+
{column_list[0]: None}, # Blank row
|
56
|
+
{**column_dict, **blank_dict},
|
57
|
+
]
|
58
|
+
|
59
|
+
if sensitivity_label:
|
60
|
+
header_data = [{column_list[0]: sensitivity_label}] + header_data
|
61
|
+
|
62
|
+
pl.DataFrame(header_data).write_csv(csv_buffer, include_header=False)
|
63
|
+
|
64
|
+
for i, data in enumerate(list_of_df_title_subtitle_dicts):
|
65
|
+
df = data["df"]
|
66
|
+
title = data["title"]
|
67
|
+
subtitle = data["subtitle"]
|
68
|
+
|
69
|
+
if i > 0 and title is not None:
|
70
|
+
column_dict = {column_name: column_name for column_name in list(df.columns)}
|
71
|
+
header_data = [
|
72
|
+
{column_list[0]: title},
|
73
|
+
*(
|
74
|
+
[{column_list[0]: subtitle}] if subtitle is not None else []
|
75
|
+
), # Uses unpacking (*) to add the subtitle row if subtitle is not None. If
|
76
|
+
# subtitle is None, it unpacks an empty list, effectively skipping the row.
|
77
|
+
{column_list[0]: None}, # Blank row
|
78
|
+
]
|
79
|
+
pl.DataFrame(header_data).write_csv(csv_buffer, include_header=False)
|
80
|
+
df.write_csv(csv_buffer, include_header=i > 0)
|
81
|
+
|
82
|
+
if i < len(list_of_df_title_subtitle_dicts) - 1:
|
83
|
+
blank_row = pl.DataFrame({df.columns[0]: [None]})
|
84
|
+
blank_row.write_csv(csv_buffer, include_header=False)
|
85
|
+
|
86
|
+
csv_buffer.seek(0)
|
87
|
+
csv_data = (
|
88
|
+
"\ufeff" + csv_buffer.getvalue()
|
89
|
+
) # Adding \ufeff ensures the correct character encoding is detected for £
|
90
|
+
|
91
|
+
return dcc.send_string(csv_data, f"{name}.csv")
|
92
|
+
|
93
|
+
|
94
|
+
def _get_number_of_max_columns_from_all_dfs(list_of_df_title_subtitle_dicts):
|
95
|
+
max_columns = 0
|
96
|
+
index_of_max_cols = -1
|
97
|
+
|
98
|
+
for idx, dic in enumerate(list_of_df_title_subtitle_dicts):
|
99
|
+
# Get the DataFrame
|
100
|
+
df = dic["df"]
|
101
|
+
|
102
|
+
# Get the number of columns
|
103
|
+
num_columns = df.shape[1]
|
104
|
+
|
105
|
+
# Update if this DataFrame has more columns
|
106
|
+
if num_columns > max_columns:
|
107
|
+
max_columns = num_columns
|
108
|
+
index_of_max_cols = idx
|
109
|
+
|
110
|
+
max_columns = len(
|
111
|
+
list(list_of_df_title_subtitle_dicts[index_of_max_cols]["df"].columns)
|
112
|
+
)
|
113
|
+
|
114
|
+
return max_columns
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: gov_uk_dashboards
|
3
|
-
Version:
|
3
|
+
Version: 17.1.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
|
@@ -1,12 +1,13 @@
|
|
1
1
|
gov_uk_dashboards/__init__.py,sha256=4y4hozrL4hzRUna8b22UQwtkZcvvCgeXsAtA6AJww8g,1299
|
2
2
|
gov_uk_dashboards/axes.py,sha256=Oh8zyWzxwZWq8bQl-vJoXwTANZ7ACZiYPmIXlM62Fvk,712
|
3
3
|
gov_uk_dashboards/colours.py,sha256=MezFI_3hgEuZ7f8F5TdtgaQO00B3G7ylRTCVq3SlSF4,2371
|
4
|
-
gov_uk_dashboards/constants.py,sha256=
|
4
|
+
gov_uk_dashboards/constants.py,sha256=O_JyW1XlNg_mHwmT2xfup6mOtfd9x44ky5APReFfOqo,686
|
5
5
|
gov_uk_dashboards/symbols.py,sha256=6_lq8yl1l7sGUlOY29wip_PXeUUoM53_fakKY1omVwY,424
|
6
6
|
gov_uk_dashboards/template.html,sha256=uFAki9bEpcei5dP_NZYP8dsxpxXpNIDQPn7ezsE7VXc,498
|
7
7
|
gov_uk_dashboards/template.py,sha256=WNVkAW3LCNoUHcxQ1kiUPkGCKIan5DgZQaAA_ZbE5WA,534
|
8
8
|
gov_uk_dashboards/assets/__init__.py,sha256=SwV4sjjMQmuZXloVcvf2H86XFiAd8xhPLJvaSMC2MQE,101
|
9
9
|
gov_uk_dashboards/assets/attach-event-to-dash.js,sha256=mbea5TaxEnjMN-MebR3J0hVjOb1B27-zXwWw9ZYqBBw,562
|
10
|
+
gov_uk_dashboards/assets/custom_map_style_functions.js,sha256=1-jnhguDEn7-BnleGbSTB-dlaSO-6hPF-JL0d5EUnhU,1689
|
10
11
|
gov_uk_dashboards/assets/dashboard.css,sha256=exnujRlse2xHOeuE_TAs84gITv5YMRzKJKEeSjoXO6U,202134
|
11
12
|
gov_uk_dashboards/assets/get_assets_folder.py,sha256=ozYOmuO2r36vaTdWMU5cBuskl47RHBp8z9KIO06BXBM,171
|
12
13
|
gov_uk_dashboards/assets/govuk-frontend-3.14.0.min.js,sha256=kKi_r2hAqumi2pKM43W8NKpR2UtksUww9lSQOjkOmMI,34806
|
@@ -34,7 +35,7 @@ gov_uk_dashboards/assets/images/oflog/how_to_5_viewing_tables.png,sha256=kC36T6T
|
|
34
35
|
gov_uk_dashboards/assets/topojson/usa_110m.json,sha256=yX3AZ1tGULJmVFyWwMkb19WdZShJbvpa97uYvldMvTk,49104
|
35
36
|
gov_uk_dashboards/assets/topojson/world_110m.json,sha256=11kV6qMchw32uXLJ5buGkQGXgl8z3P73QPOy9oz_6EM,136642
|
36
37
|
gov_uk_dashboards/components/__init__.py,sha256=19fr4E5-FkG_rXAPwRc2Gh60ncgc7Ho1WdrMV5x9frw,187
|
37
|
-
gov_uk_dashboards/components/dash/__init__.py,sha256=
|
38
|
+
gov_uk_dashboards/components/dash/__init__.py,sha256=0Sunv0rmcn-LevR17i90Qr542BLek3EZwmiP_uaJJHA,4830
|
38
39
|
gov_uk_dashboards/components/dash/apply_and_reset_filters_buttons.py,sha256=I7VoKOS_zIYdQvDP3SkAOntDMxZSQKCxnY80173EVz8,673
|
39
40
|
gov_uk_dashboards/components/dash/banners.py,sha256=TDHdJQSb7mCqtmM6ylSnVh_Qpd2Aec1jSR8RfsjqQoE,958
|
40
41
|
gov_uk_dashboards/components/dash/card.py,sha256=aErz9h536nYvpp9LZsFHo_Rn5rRXXfyGh358VcLksaE,420
|
@@ -42,11 +43,9 @@ gov_uk_dashboards/components/dash/card_full_width.py,sha256=ItwNgb5YQ1n6Fznvy_ng
|
|
42
43
|
gov_uk_dashboards/components/dash/collapsible_panel.py,sha256=WrC5IFzM9mNavPfeSpBvMvYQfrmfhpTPCG1Ay70Jtw8,1191
|
43
44
|
gov_uk_dashboards/components/dash/comparison_la_filter_button.py,sha256=mhtmYVyToabuYzal00cB07YfKSx_wjDfjB8W-IKDXPc,670
|
44
45
|
gov_uk_dashboards/components/dash/context_banner.py,sha256=2o44AzS0pTYTY0r3OVbKW7dJg2lA1J8PW4RCeGQ15gg,1010
|
45
|
-
gov_uk_dashboards/components/dash/create_download_chart_button.py,sha256=yb3TbMHxQmC7XFojwnZMBMpwMal9NUcT4LNt9ZccU6Q,711
|
46
|
-
gov_uk_dashboards/components/dash/create_download_data_button.py,sha256=ng57WLkA5wyfrLTrO05VJ6LmC_gwgODQEYpMj-v-wO8,714
|
47
46
|
gov_uk_dashboards/components/dash/dashboard_container.py,sha256=KC2isR0NShxUYCl_pzDEAS4WK5pFrLMp4m2We3AqiwM,512
|
48
47
|
gov_uk_dashboards/components/dash/details.py,sha256=-Rgat95pCnU3fF4O0MkyymzMQPCZhRvuq0U0a5gHZBU,1106
|
49
|
-
gov_uk_dashboards/components/dash/download_button.py,sha256=
|
48
|
+
gov_uk_dashboards/components/dash/download_button.py,sha256=PpvZMmf0R7qI5_xZgPve7JDCVONC07KrLJg8IpXoyCE,1781
|
50
49
|
gov_uk_dashboards/components/dash/filter_panel.py,sha256=lQZYc5g7xUOpHfLWpPXiBKkIRKXoFb1p9NhJgS21tNo,1996
|
51
50
|
gov_uk_dashboards/components/dash/footer.py,sha256=RnJKN1YTP88GuJ4e7ci2T-jDqIe0-jDuHAQ8OhSCK50,3155
|
52
51
|
gov_uk_dashboards/components/dash/graph.py,sha256=bd49W5sVyhtWd4lNBfQST1RyLNlTLA0KRxS7jTgVMwE,886
|
@@ -70,16 +69,19 @@ gov_uk_dashboards/components/dash/visualisation_commentary.py,sha256=jBy8qy2DWYq
|
|
70
69
|
gov_uk_dashboards/components/dash/visualisation_title.py,sha256=xXHsyVRIGkngBXCuhv8z0PmZLOeicjpMvmTbA5HhRvQ,561
|
71
70
|
gov_uk_dashboards/components/dash/warning_text.py,sha256=31XvPUINt2QsWIaaMnqEvn23G-MpzWbDvGnzYcXIyAY,1622
|
72
71
|
gov_uk_dashboards/components/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
73
|
-
gov_uk_dashboards/components/helpers/display_chart_or_table_with_header.py,sha256=
|
72
|
+
gov_uk_dashboards/components/helpers/display_chart_or_table_with_header.py,sha256=4Kh_qaFZUqQrS08Bb7j9T-ltSVjLtI_EOnpXY-uMvVc,2918
|
74
73
|
gov_uk_dashboards/components/helpers/generate_dash_graph_from_figure.py,sha256=sdhC6Mjfw6kqs1MDRDoMuOt8dNS9Bl1WEoJX9S5AssA,1813
|
74
|
+
gov_uk_dashboards/components/helpers/get_chart_for_download.py,sha256=RS5SR2tCQPTkBpWaIr2mb98Yn6vAimBnAXpRhMx0kB4,669
|
75
75
|
gov_uk_dashboards/components/helpers/plotting_helper_functions.py,sha256=moD2tse2FqTBW2rOHOvZCL9BIlmDbyvU5233yFFu_aI,1635
|
76
76
|
gov_uk_dashboards/components/helpers/update_layout_bgcolor_margin.py,sha256=i7Nwp0CxFpkyQeR8KfOBVMBkzctG7hMpWI2OzgxB2jY,740
|
77
|
+
gov_uk_dashboards/components/leaflet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
78
|
+
gov_uk_dashboards/components/leaflet/leaflet_choropleth_map.py,sha256=4ysc-FxRKGPSkNuJBDtjlEOGthC7EuIliaillqX2GeI,6221
|
77
79
|
gov_uk_dashboards/components/plotly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
78
80
|
gov_uk_dashboards/components/plotly/captioned_figure.py,sha256=T0sbtGTiJ79FXxVdPb__hqISuyTc3Dl11cKhgcuW-5U,2804
|
79
81
|
gov_uk_dashboards/components/plotly/choropleth_map.py,sha256=U9RmS3MZGloQAt9HoSYh3Xad205DDfZOjz91ZD_ydbI,9849
|
80
82
|
gov_uk_dashboards/components/plotly/enums.py,sha256=ebHiFQljA7O4HJxKoslqlNXcAjUZi1askpTSwxKEcYQ,850
|
81
|
-
gov_uk_dashboards/components/plotly/stacked_barchart.py,sha256
|
82
|
-
gov_uk_dashboards/components/plotly/time_series_chart.py,sha256=
|
83
|
+
gov_uk_dashboards/components/plotly/stacked_barchart.py,sha256=-pFwY3vhjQrLR_innzbs4d-vqWSXE_eJS-ceP5wo_SI,12530
|
84
|
+
gov_uk_dashboards/components/plotly/time_series_chart.py,sha256=QD9HjXLjNIBA-NBZQUSL3TyIAvRGrBzwnYrRboROECg,23002
|
83
85
|
gov_uk_dashboards/figures/__init__.py,sha256=_snQeNlM81nNKERl4gg9ScH2HYbtwaBjl8yQonccx34,712
|
84
86
|
gov_uk_dashboards/figures/chart_data.py,sha256=fEsNkQFzXKIQ0h7aLBWq3J1qAxHbxvJeKU23JrVodVc,757
|
85
87
|
gov_uk_dashboards/figures/line_chart.py,sha256=rEB51_z9cPl7BcT94iA6ZsZXzecnVCnGpQWW_9SNGUY,2813
|
@@ -89,6 +91,7 @@ gov_uk_dashboards/figures/styles/__init__.py,sha256=wVa8BU0TvXnanksOUVWN4utFKW4O
|
|
89
91
|
gov_uk_dashboards/figures/styles/line_style.py,sha256=sXc9pGrAVxFrxqXSTsrw0hEgyhmhMOLhqhwx93J-PtI,559
|
90
92
|
gov_uk_dashboards/formatting/__init__.py,sha256=bQk9_OEubUhuTRQjXUs4ZItgSY7yyDpwQcLauxf11Tk,460
|
91
93
|
gov_uk_dashboards/formatting/human_readable.py,sha256=e-ROPS7oFLHCV58MXBIhLsZQut-F-bAYz0TOz33gX2M,1844
|
94
|
+
gov_uk_dashboards/formatting/number_formatting.py,sha256=9LVqKqx0mMQyv8XLI_im1pdASZDJxRjzRWtRN-g6kPs,913
|
92
95
|
gov_uk_dashboards/formatting/round_and_add_prefix_and_suffix.py,sha256=_DboRfdvwb9Y62H9LFDFJ0ju4626z-_oFuwagRgyZDY,1456
|
93
96
|
gov_uk_dashboards/formatting/rounding.py,sha256=Em1yri_j18IYHbZ64d3bhVKX-XEllRSM9FzAUo1o6fU,968
|
94
97
|
gov_uk_dashboards/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -100,8 +103,11 @@ gov_uk_dashboards/lib/dap/dap_deployment.py,sha256=ZXixeOAtRNjMsPdGKLwwLNamlo0mi
|
|
100
103
|
gov_uk_dashboards/lib/dap/get_dataframe_from_cds.py,sha256=OiusRCgYnkBjK_GZgYLGzNrxOGizYt8CgThiWRCVKK0,3921
|
101
104
|
gov_uk_dashboards/lib/datetime_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
102
105
|
gov_uk_dashboards/lib/datetime_functions/datetime_functions.py,sha256=BQgr8I_vFNYwLi-fE4YC6jZ5PpbPea2rnD_2a_lw0YE,12209
|
103
|
-
gov_uk_dashboards
|
104
|
-
gov_uk_dashboards
|
105
|
-
gov_uk_dashboards
|
106
|
-
gov_uk_dashboards-
|
107
|
-
gov_uk_dashboards-
|
106
|
+
gov_uk_dashboards/lib/download_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
|
+
gov_uk_dashboards/lib/download_functions/convert_fig_to_image_and_download.py,sha256=JYDpWObBsiKG5Rtk2ElOTgHwfIR0cRmVocr8RlTfPZQ,534
|
108
|
+
gov_uk_dashboards/lib/download_functions/download_csv_with_headers.py,sha256=h50ejODCjoz9z-yqDt6nsE6jN6XxJN1DWH66CjLJiCk,4155
|
109
|
+
gov_uk_dashboards-17.1.0.dist-info/licenses/LICENSE,sha256=GDiD7Y2Gx7JucPV1JfVySJeah-qiSyBPdpJ6RHCEHTc,1126
|
110
|
+
gov_uk_dashboards-17.1.0.dist-info/METADATA,sha256=6wQUDbkJ1690nFpTKm5XfCPwIDhvWGgXONm6bI8xoA8,5917
|
111
|
+
gov_uk_dashboards-17.1.0.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
112
|
+
gov_uk_dashboards-17.1.0.dist-info/top_level.txt,sha256=gPaN1P3-H3Rgi2me6tt-fX_cxo19CZfA4PjlZPjGRpo,18
|
113
|
+
gov_uk_dashboards-17.1.0.dist-info/RECORD,,
|
@@ -1,25 +0,0 @@
|
|
1
|
-
"""create_download_chart_button"""
|
2
|
-
|
3
|
-
from dash import html
|
4
|
-
from gov_uk_dashboards.components.dash import download_button_with_icon
|
5
|
-
|
6
|
-
|
7
|
-
def create_download_chart_button(button_id_name: str) -> html.Div:
|
8
|
-
"""Create a download button for charts, aligned to the right.
|
9
|
-
|
10
|
-
Parameters:
|
11
|
-
- button_id_name (str): A unique identifier for the button.
|
12
|
-
|
13
|
-
Returns:
|
14
|
-
- html.Div: A Div element containing a download button.
|
15
|
-
"""
|
16
|
-
return html.Div(
|
17
|
-
download_button_with_icon(
|
18
|
-
button_text="Download chart",
|
19
|
-
button_id={
|
20
|
-
"download-type": "download-chart",
|
21
|
-
"name": button_id_name,
|
22
|
-
},
|
23
|
-
),
|
24
|
-
style={"textAlign": "right"},
|
25
|
-
)
|
@@ -1,25 +0,0 @@
|
|
1
|
-
"""create_download_data_button"""
|
2
|
-
|
3
|
-
from dash import html
|
4
|
-
from gov_uk_dashboards.components.dash import download_button_with_icon
|
5
|
-
|
6
|
-
|
7
|
-
def create_download_data_button(button_id_name: str) -> html.Div:
|
8
|
-
"""Create a download button for data, aligned to the right.
|
9
|
-
|
10
|
-
Parameters:
|
11
|
-
- button_id_name (str): A unique identifier for the button.
|
12
|
-
|
13
|
-
Returns:
|
14
|
-
- html.Div: A Div element containing a download button.
|
15
|
-
"""
|
16
|
-
return html.Div(
|
17
|
-
download_button_with_icon(
|
18
|
-
button_text=".csv",
|
19
|
-
button_id={
|
20
|
-
"download-type": "download-data",
|
21
|
-
"name": button_id_name,
|
22
|
-
},
|
23
|
-
),
|
24
|
-
style={"float": "left", "margin-top": "-20px"},
|
25
|
-
)
|
File without changes
|
File without changes
|