Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Imports:
azkit (>= 0.3.1),
AzureStor,
bs4Dash,
cachem,
config (>= 0.3.1),
dplyr,
DT,
Expand All @@ -38,15 +37,12 @@ Imports:
httr2,
jsonlite,
jsonvalidate,
leaflet,
markdown,
openssl,
promises,
purrr,
readr,
rlang,
scales,
sf,
shiny (>= 1.14.0),
shinycssloaders,
shinyjs,
Expand All @@ -56,7 +52,6 @@ Imports:
tibble,
tidyr,
tidyselect,
viridis,
zeallot
Suggests:
covr,
Expand Down
10 changes: 10 additions & 0 deletions R/ZZZ.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,13 @@ get_params_schema <- function() {
}
.schema_cache[["schema"]]
}

use_leafletjs <- function() {
htmltools::htmlDependency(
name = "leaflet",
version = "1.9.4",
src = c(href = "https://unpkg.com/leaflet@1.9.4/dist"),
script = "leaflet.js",
stylesheet = "leaflet.css"
Comment thread
tomjemmett marked this conversation as resolved.
)
}
2 changes: 2 additions & 0 deletions R/app_ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -339,12 +339,14 @@ golem_add_external_resources <- function() {
"www",
app_sys("app/www")
)

tags$head(
golem::favicon(),
golem::bundle_resources(
path = app_sys("app/www"),
app_title = "NHP: Inputs"
),
use_leafletjs(),
tags$base(target = "_blank")
)
}
7 changes: 1 addition & 6 deletions R/fct_utils_lookups.R
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,6 @@ get_waiting_list_multipliers <- function() {
)
}

get_icb_boundaries <- function() {
sf::read_sf(lookup_file_path("icb_boundaries.geojson"))
}

# use a singleton pattern to cache lookups in memory, but prevent the files from
# being read immediately when the package is attached
.lookups_cache <- new.env()
Expand All @@ -99,8 +95,7 @@ get_lookups <- function() {
"ndg_variants" = get_ndg_variants_lookup(),
"nee_table" = get_nee_lookup(),
"rtt_specialties" = get_rtt_specialties_lookup(),
"waiting_list_multipliers" = get_waiting_list_multipliers(),
"icb_boundaries" = get_icb_boundaries()
"waiting_list_multipliers" = get_waiting_list_multipliers()
),
envir = .lookups_cache
)
Expand Down
24 changes: 9 additions & 15 deletions R/mod_expat_repat_server.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ mod_expat_repat_server <- function(id, params) {
dplyr::select("specialty", "code") |>
tibble::deframe()

icb_boundaries <- get_lookups()[["icb_boundaries"]]

mod_reasons_server(shiny::NS(id, "reasons"), params, "expat_repat")

shiny::moduleServer(id, function(input, output, session) {
Expand Down Expand Up @@ -331,24 +329,20 @@ mod_expat_repat_server <- function(id, params) {
mod_expat_repat_nonlocal_n(df)
})

output$repat_nonlocal_icb_map <- leaflet::renderLeaflet({
dat <- repat_nonlocal() |>
shiny::observe({
icb_pcnts <- repat_nonlocal() |>
dplyr::filter(
.data$count > 5,
.data$fyear == params$start_year,
.data[["count"]] > 5,
.data[["fyear"]] == params$start_year,
.data[["provider"]] == params$dataset,
!.data[["is_main_icb"]]
)

df <- icb_boundaries |>
dplyr::inner_join(
dat,
by = dplyr::join_by("icb22cdh" == "icb")
)
) |>
dplyr::select("icb", "pcnt") |>
tibble::deframe()

shiny::req(nrow(df) > 0)
shiny::req(length(icb_pcnts) > 0)

mod_expat_repat_nonlocal_icb_map(df)
session$sendCustomMessage("selectedIcbs", icb_pcnts)
})
Comment thread
tomjemmett marked this conversation as resolved.

# return ----
Expand Down
7 changes: 3 additions & 4 deletions R/mod_expat_repat_ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,9 @@ mod_expat_repat_ui <- function(id) {
)
),
col_4(
shinycssloaders::withSpinner(
leaflet::leafletOutput(
ns("repat_nonlocal_icb_map")
)
shiny::tags$div(
id = ns("icb_map"),
style = "height: 400px;"
)
)
)
Expand Down
20 changes: 0 additions & 20 deletions R/mod_expat_repat_utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,3 @@ mod_expat_repat_nonlocal_n <- function(df) {
panel.grid.major.y = ggplot2::element_line("#9d928a", linetype = "dotted")
)
}


mod_expat_repat_nonlocal_icb_map <- function(df) {
# nolint start: object_usage_linter
pal <- leaflet::colorNumeric(
viridis::viridis_pal()(3),
df$pcnt
)
# nolint end

leaflet::leaflet(df) |>
leaflet::addProviderTiles("CartoDB.Positron") |>
leaflet::addPolygons(
color = "#000000",
weight = 1,
opacity = 1,
fillColor = ~ pal(pcnt),
popup = ~ glue::glue("{icb22nm}: {count} ({scales::percent(pcnt)})")
)
}
4 changes: 3 additions & 1 deletion R/mod_home_server.R
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ mod_home_server <- function(id, filename) {
purrr::walk(names(p), \(i) params[[i]] <- p[[i]])

# remove the temporary file
unlink(filename())
if (!shiny::in_devmode()) {
unlink(filename())
}

init$destroy()
})
Expand Down

Large diffs are not rendered by default.

188 changes: 188 additions & 0 deletions inst/app/www/map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
document.addEventListener('DOMContentLoaded', function () {
const mapContainerId = 'expat_repat-icb_map';
const mapEl = document.getElementById(mapContainerId);

if (!mapEl) {
console.error(`Map container not found: #${mapContainerId}`);
return;
}

if (mapEl.getBoundingClientRect().height === 0) {
console.warn(
`Map container #${mapContainerId} has zero height. Set an explicit CSS height.`
);
}

// Initialize the map and centre it roughly on the UK
const map = L.map(mapContainerId).setView([54.7, -2.6], 6);
let resizeScheduled = false;
// set up some variables which we will load values into later
let icbBoundaries = null;
let icbLayer = null;
let selectedIcbs = {};

// Add a tile layer to the map (using Carto's light basemap)
L.tileLayer(
'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{
subdomains: 'abcd',
maxZoom: 10,
attribution: '&copy; OpenStreetMap contributors &copy; CARTO'
}
).addTo(map);

const viridisStops = ['#440154', '#21908c', '#fde725'];

function hexToRgb(hex) {
const clean = hex.replace('#', '');
return {
r: parseInt(clean.slice(0, 2), 16),
g: parseInt(clean.slice(2, 4), 16),
b: parseInt(clean.slice(4, 6), 16)
};
}

function rgbToHex({ r, g, b }) {
return '#' + [r, g, b]
.map((x) => Math.round(x).toString(16).padStart(2, '0'))
.join('');
}

function interpolateColour(a, b, t) {
const c1 = hexToRgb(a);
const c2 = hexToRgb(b);

return rgbToHex({
r: c1.r + (c2.r - c1.r) * t,
g: c1.g + (c2.g - c1.g) * t,
b: c1.b + (c2.b - c1.b) * t
});
}

function viridisColour(value) {
if (!Number.isFinite(value)) {
return '#cccccc';
}

const clamped = Math.max(0, Math.min(1, value));
const scaled = clamped * (viridisStops.length - 1);
const idx = Math.min(Math.floor(scaled), viridisStops.length - 2);
const t = scaled - idx;

return interpolateColour(viridisStops[idx], viridisStops[idx + 1], t);
}

function fillColour(icbCode) {
let pcnt = selectedIcbs?.[icbCode];
let total = Object.values(selectedIcbs).reduce((a, v) => a + v, 0);

return viridisColour(pcnt / total);
}

function invalidateMapSize() {
if (resizeScheduled) {
return;
}

resizeScheduled = true;
requestAnimationFrame(() => {
resizeScheduled = false;
map.invalidateSize();
});
}

// Function to render the points for the selected peers on the map
function renderIcbLayer() {
// check that the icbBoundaries has been loaded before trying to filter and render it
if (!icbBoundaries) {
return;
}

// the icbBoundaries contains all the providers, we filter it to only include the current
// provider and its peers
const filteredFeatures = icbBoundaries.features.filter((feature) => {
// check that shiny has sent the peers to filter to. if it hasn't yet, don't show any points
if (!selectedIcbs || Object.keys(selectedIcbs).length === 0) {
return false;
}
// check if the current feature is in the selected peers list
return Object.hasOwn(selectedIcbs, feature.properties.icb22cdh);
});

// remove the existing icbLayer from the map if it exists, so we can replace it with the
// new filtered layer
if (icbLayer) {
map.removeLayer(icbLayer);
}

// generate a new layer with the filtered features and add it to the map. this layer will be
// styled with circle markers and popups for each provider's name, coloured based on whether it
// is the selected dataset or a peer.
icbLayer = L.geoJSON(
{
type: 'FeatureCollection',
features: filteredFeatures
},
{
style: function (feature) {
return {
fillColor: fillColour(feature.properties.icb22cdh),
color: '#000000',
weight: 1,
opacity: 1,
fillOpacity: 0.7
}
},
onEachFeature: function (feature, layer) {
if (feature.properties && feature.properties.icb22nm) {
const pcnt = selectedIcbs[feature.properties.icb22cdh];
layer.bindPopup(`${feature.properties.icb22nm}: ${(pcnt * 100).toFixed(1)}%`);
}
Comment thread
Copilot marked this conversation as resolved.
}
}

).addTo(map);

invalidateMapSize();

const bounds = icbLayer.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, {
padding: [10, 10]
});
}
}

// asynchronously load the icb_boundaries.geojson file and store it in the icbBoundaries
// variable. once loaded, call renderIcbLayer to display the points on the map.
fetch('www/icb_boundaries.geojson')
.then((response) => {
if (!response.ok) {
throw new Error('Failed to load icb_boundaries.geojson');
}
return response.json();
})
.then((geojson) => {
icbBoundaries = geojson;
renderIcbLayer();
})
.catch((error) => {
console.error('Error loading icb_boundaries.geojson:', error);
});

// listen for messages from shiny containing the selected peers. when received, store the org_ids
// in a Set and call renderProvidersLayer to update the map with the new selection.
Shiny.addCustomMessageHandler('selectedIcbs', function (message) {
selectedIcbs = message;
renderIcbLayer();
});

const resizeObserver = new ResizeObserver(() => {
if (mapEl.getBoundingClientRect().height > 0) {
invalidateMapSize();
}
});

resizeObserver.observe(mapEl);

});