-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Browse page selected layers #521
Changes from 9 commits
f42ea99
b9485cd
981b6b5
17a1a98
0abcb2c
1ee2d8e
ecf9af8
7eed7f6
37d57f2
4602f43
51e7749
7036b30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<script lang="ts"> | ||
import { activeLayers, layerLegends } from "./stores"; | ||
|
||
let container: HTMLDivElement | null = null; | ||
|
||
function update( | ||
container: HTMLDivElement | null, | ||
activeLayers: Set<string>, | ||
layerLegends: Map<string, HTMLDivElement>, | ||
) { | ||
if (container) { | ||
container.innerHTML = ""; | ||
for (let l of activeLayers) { | ||
let obj = layerLegends.get(l); | ||
// Layers can be immediately active from the URL, before the DOM element is registered | ||
if (obj) { | ||
obj.style.display = "block"; | ||
container.appendChild(obj); | ||
} | ||
} | ||
} | ||
} | ||
$: update(container, $activeLayers, $layerLegends); | ||
</script> | ||
|
||
<p><b>Selected layers</b></p> | ||
<div bind:this={container} /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
<script lang="ts"> | ||
import { HelpButton } from "lib/common"; | ||
import { SecondaryButton, Checkbox, CheckboxGroup } from "govuk-svelte"; | ||
import { onDestroy } from "svelte"; | ||
import { activeLayers, layerLegends } from "./stores"; | ||
|
||
// Slots include: icon, controls, help | ||
|
||
export let name: string; | ||
export let title: string; | ||
// This must be bound to state that controls the layer display and URL | ||
export let show: boolean; | ||
|
||
// If the layer is initially shown (from the URL state), make it active | ||
if (show) { | ||
$activeLayers.add(name); | ||
$activeLayers = $activeLayers; | ||
} | ||
|
||
// The controls for each layer are defined in a layer component's optional | ||
// "controls" slot, as well as the generic controls defined below in | ||
// this component. The whole DOM element is bound here, then it's | ||
// stored centrally in layerLegends . That lets ActiveLayersLegend | ||
// later display the proper layer controls in one place. | ||
let contents: HTMLDivElement | null = null; | ||
|
||
$: if (contents) { | ||
layerLegends.update((l) => { | ||
// Initially hide the controls. Since layer components are defined under | ||
// the left sidebar, they'd otherwise appear there. | ||
contents!.style.display = "none"; | ||
l.set(name, contents!); | ||
return l; | ||
}); | ||
} | ||
|
||
onDestroy(() => { | ||
layerLegends.update((l) => { | ||
l.delete(name); | ||
return l; | ||
}); | ||
}); | ||
|
||
function toggleActive() { | ||
activeLayers.update((l) => { | ||
if (l.has(name)) { | ||
l.delete(name); | ||
show = false; | ||
} else { | ||
l.add(name); | ||
show = true; | ||
} | ||
return l; | ||
}); | ||
} | ||
|
||
function remove() { | ||
$activeLayers.delete(name); | ||
$activeLayers = $activeLayers; | ||
show = false; | ||
} | ||
</script> | ||
|
||
<!-- For the left sidebar --> | ||
<Checkbox checked={$activeLayers.has(name)} on:change={toggleActive}> | ||
{title} | ||
</Checkbox> | ||
|
||
<!-- For the right panel --> | ||
<div bind:this={contents}> | ||
<CheckboxGroup small> | ||
<div | ||
style="display: flex; justify-content: space-between; white-space: nowrap" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nowrap thing is very weird and messes up vertical alignment. I can't make everything behave here, but we can iterate on it later |
||
> | ||
<Checkbox bind:checked={show}> | ||
<slot name="icon" /> | ||
{title} | ||
</Checkbox> | ||
<span> | ||
<HelpButton> | ||
<slot name="help" /> | ||
</HelpButton> | ||
|
||
<SecondaryButton on:click={remove}>X</SecondaryButton> | ||
</span> | ||
</div> | ||
</CheckboxGroup> | ||
{#if show} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is flexible. We might try using a collapsible card next. It should be easy to plug things in here and make them work for all layers |
||
<slot name="controls" /> | ||
{/if} | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,6 @@ | ||
<script lang="ts"> | ||
import { | ||
ExternalLink, | ||
HelpButton, | ||
Popup, | ||
publicResourceBaseUrl, | ||
} from "lib/common"; | ||
import { Checkbox } from "govuk-svelte"; | ||
import { ExternalLink, Popup, publicResourceBaseUrl } from "lib/common"; | ||
import { Radio } from "govuk-svelte"; | ||
import { layerId, makeColorRamp } from "lib/maplibre"; | ||
import { | ||
FillLayer, | ||
|
@@ -18,6 +13,7 @@ | |
import OsOglLicense from "../OsOglLicense.svelte"; | ||
import SequentialLegend from "../SequentialLegend.svelte"; | ||
import { customUrlState } from "../url"; | ||
import LayerControl from "../LayerControl.svelte"; | ||
|
||
let name = "census_output_areas"; | ||
let colorScale = colors.sequential_low_to_high; | ||
|
@@ -28,7 +24,7 @@ | |
}; | ||
let defaultState = { | ||
show: false, | ||
kind: "", | ||
kind: "percent_households_with_car", | ||
}; | ||
function stringify(x: State): string | null { | ||
return x.show ? x.kind : null; | ||
|
@@ -41,26 +37,6 @@ | |
} | ||
let state = customUrlState(name, defaultState, stringify, parse); | ||
|
||
// Mutually exclusive, like a radio button. We need these for checkboxes to work | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Big change to this layer to use radio buttons. Probably should've been like this from the start |
||
let showHouseholdsWithCar = $state.kind == "percent_households_with_car"; | ||
let showAverageCars = $state.kind == "average_cars_per_household"; | ||
let showPopulationDensity = $state.kind == "population_density"; | ||
$: { | ||
if (showHouseholdsWithCar) { | ||
$state.show = true; | ||
$state.kind = "percent_households_with_car"; | ||
} else if (showAverageCars) { | ||
$state.show = true; | ||
$state.kind = "average_cars_per_household"; | ||
} else if (showPopulationDensity) { | ||
$state.show = true; | ||
$state.kind = "population_density"; | ||
} else { | ||
$state.show = false; | ||
$state.kind = ""; | ||
} | ||
} | ||
|
||
function onClick(e: CustomEvent<LayerClickInfo>) { | ||
let oa = e.detail.features[0].properties!["OA21CD"]; | ||
if ( | ||
|
@@ -94,16 +70,9 @@ | |
} | ||
</script> | ||
|
||
<Checkbox | ||
bind:checked={showHouseholdsWithCar} | ||
on:change={() => { | ||
showAverageCars = false; | ||
showPopulationDensity = false; | ||
}} | ||
> | ||
Percent of households with a car | ||
<span slot="right"> | ||
<HelpButton> | ||
<LayerControl {name} title="2021 census" bind:show={$state.show}> | ||
<span slot="help"> | ||
{#if $state.kind == "percent_households_with_car"} | ||
<p> | ||
Car/van availability data is from the 2021 census, via <ExternalLink | ||
href="https://www.nomisweb.co.uk/sources/census_2021_bulk" | ||
|
@@ -115,31 +84,7 @@ | |
ONS Geography | ||
</ExternalLink>. | ||
</p> | ||
<p> | ||
License: <ExternalLink | ||
href="http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" | ||
> | ||
Open Government License | ||
</ExternalLink>. Contains OS data © Crown copyright and database | ||
right 2023. | ||
</p> | ||
</HelpButton> | ||
</span> | ||
</Checkbox> | ||
{#if $state.kind == "percent_households_with_car"} | ||
<SequentialLegend {colorScale} limits={makeLimits($state.kind)} /> | ||
{/if} | ||
|
||
<Checkbox | ||
bind:checked={showAverageCars} | ||
on:change={() => { | ||
showHouseholdsWithCar = false; | ||
showPopulationDensity = false; | ||
}} | ||
> | ||
Average cars per household | ||
<span slot="right"> | ||
<HelpButton> | ||
{:else if $state.kind == "average_cars_per_household"} | ||
<p> | ||
Where the census counts "3 or more cars or vans", the average shown here | ||
assumes 3. | ||
|
@@ -155,31 +100,7 @@ | |
ONS Geography | ||
</ExternalLink>. | ||
</p> | ||
<p> | ||
License: <ExternalLink | ||
href="http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" | ||
> | ||
Open Government License | ||
</ExternalLink>. Contains OS data © Crown copyright and database | ||
right 2023. | ||
</p> | ||
</HelpButton> | ||
</span> | ||
</Checkbox> | ||
{#if $state.kind == "average_cars_per_household"} | ||
<SequentialLegend {colorScale} limits={makeLimits($state.kind)} /> | ||
{/if} | ||
|
||
<Checkbox | ||
bind:checked={showPopulationDensity} | ||
on:change={() => { | ||
showHouseholdsWithCar = false; | ||
showAverageCars = false; | ||
}} | ||
> | ||
Population density | ||
<span slot="right"> | ||
<HelpButton> | ||
{:else} | ||
<p> | ||
Population density data is from the 2021 census, via <ExternalLink | ||
href="https://www.nomisweb.co.uk/sources/census_2021_bulk" | ||
|
@@ -191,17 +112,34 @@ | |
ONS Geography | ||
</ExternalLink>. | ||
</p> | ||
<OsOglLicense /> | ||
</HelpButton> | ||
{/if} | ||
<OsOglLicense /> | ||
</span> | ||
</Checkbox> | ||
{#if $state.kind == "population_density"} | ||
<p>(people per square kilometres)</p> | ||
<SequentialLegend | ||
{colorScale} | ||
limits={makeLimits($state.kind).map((x) => x.toLocaleString())} | ||
/> | ||
{/if} | ||
|
||
<span slot="controls"> | ||
<Radio | ||
label="Dataset" | ||
choices={[ | ||
["percent_households_with_car", "Percent of households with a car"], | ||
["average_cars_per_household", "Average cars per household"], | ||
["population_density", "Population density"], | ||
]} | ||
bind:value={$state.kind} | ||
/> | ||
|
||
{#if $state.kind == "percent_households_with_car"} | ||
<SequentialLegend {colorScale} limits={makeLimits($state.kind)} /> | ||
{:else if $state.kind == "average_cars_per_household"} | ||
<SequentialLegend {colorScale} limits={makeLimits($state.kind)} /> | ||
{:else if $state.kind == "population_density"} | ||
<p>(people per square kilometres)</p> | ||
<SequentialLegend | ||
{colorScale} | ||
limits={makeLimits($state.kind).map((x) => x.toLocaleString())} | ||
/> | ||
{/if} | ||
</span> | ||
</LayerControl> | ||
|
||
<VectorTileSource | ||
url={`pmtiles://${publicResourceBaseUrl()}/v1/${name}.pmtiles`} | ||
|
@@ -218,7 +156,7 @@ | |
"fill-opacity": hoverStateFilter(0.5, 0.7), | ||
}} | ||
layout={{ | ||
visibility: $state.kind != "" ? "visible" : "none", | ||
visibility: $state.show ? "visible" : "none", | ||
}} | ||
eventsIfTopMost | ||
manageHoverState | ||
|
@@ -255,7 +193,7 @@ | |
"line-width": 0.5, | ||
}} | ||
layout={{ | ||
visibility: $state.kind != "" ? "visible" : "none", | ||
visibility: $state.show ? "visible" : "none", | ||
}} | ||
/> | ||
</VectorTileSource> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,11 @@ | ||
<script lang="ts"> | ||
import LayerControl from "../LayerControl.svelte"; | ||
import { | ||
ColorLegend, | ||
ExternalLink, | ||
HelpButton, | ||
Popup, | ||
publicResourceBaseUrl, | ||
} from "lib/common"; | ||
import { Checkbox } from "govuk-svelte"; | ||
import { layerId } from "lib/maplibre"; | ||
import { | ||
FillLayer, | ||
|
@@ -20,6 +19,7 @@ | |
import { showHideLayer } from "../url"; | ||
|
||
let name = "combined_authorities"; | ||
let title = "Combined authorities"; | ||
let color = colors.combined_authorities; | ||
|
||
let show = showHideLayer(name); | ||
|
@@ -34,22 +34,19 @@ | |
} | ||
</script> | ||
|
||
<Checkbox bind:checked={$show}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lots of boilerplate disappears |
||
<ColorLegend {color} /> | ||
Combined authorities | ||
<span slot="right"> | ||
<HelpButton> | ||
<p> | ||
Data from <ExternalLink | ||
href="https://geoportal.statistics.gov.uk/datasets/ons::combined-authorities-december-2022-boundaries-en-buc/explore" | ||
> | ||
ONS Geography | ||
</ExternalLink>, as of December 2022. | ||
</p> | ||
<OsOglLicense /> | ||
</HelpButton> | ||
<LayerControl {name} {title} bind:show={$show}> | ||
<span slot="icon"><ColorLegend {color} /></span> | ||
<span slot="help"> | ||
<p> | ||
Data from <ExternalLink | ||
href="https://geoportal.statistics.gov.uk/datasets/ons::combined-authorities-december-2022-boundaries-en-buc/explore" | ||
> | ||
ONS Geography | ||
</ExternalLink>, as of December 2022. | ||
</p> | ||
<OsOglLicense /> | ||
</span> | ||
</Checkbox> | ||
</LayerControl> | ||
|
||
<GeoJSON data={`${publicResourceBaseUrl()}/v1/${name}.geojson`}> | ||
<FillLayer | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a trick I've been using in many other projects. One Svelte component logically grouping some functionality can define some controls that wind up in different parts of the page. In this case, we're going a bit beyond that by centrally collecting all of the controls by name.