diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..22ed8357 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,22 @@ +# Census-Atlas architecture + +A prototype for exploring neighbourhood level Census data tables on a map. + +## Svelte architecture + +### Presentation layer ******\_\_\_\_****** + +Files: \*.svelte +Responsibilities: Displaying the current state of the app according to the designs from UI/UX + +### Model layer **********\_\_********** + +Files: \*Store.js +Responsibilities: Storing the current state of the app. Provide action methods to update state. + +### Data layer **********\_\_\_\_********** + +Files: \*Service.js +Responsibilities: Fetch data to update the model + +`npm run storybook` – fires up [Storybook](https://storybook.js.org/docs/react/writing-stories/introduction) diff --git a/babel.config.js b/babel.config.cjs similarity index 100% rename from babel.config.js rename to babel.config.cjs diff --git a/jest.config.js b/jest.config.cjs similarity index 100% rename from jest.config.js rename to jest.config.cjs diff --git a/package-lock.json b/package-lock.json index f2659438..06f3dee7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1199,9 +1199,9 @@ } }, "@babel/runtime-corejs3": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.0.tgz", - "integrity": "sha512-Oi2qwQ21X7/d9gn3WiwkDTJmq3TQtYNz89lRnoFy8VeZpWlsyXvzSwiRrRZ8cXluvSwqKxqHJ6dBd9Rv+p0ZGQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.3.tgz", + "integrity": "sha512-IAdDC7T0+wEB4y2gbIL0uOXEYpiZEeuFUTVbdGq+UwCcF35T/tS8KrmMomEwEc5wBbyfH3PJVpTSUqrhPDXFcQ==", "dev": true, "requires": { "core-js-pure": "^3.19.0", @@ -5868,9 +5868,9 @@ "dev": true }, "@types/prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.2.tgz", + "integrity": "sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA==", "dev": true }, "@types/pretty-hrtime": { @@ -12347,17 +12347,6 @@ "@jest/core": "^27.3.1", "import-local": "^3.0.2", "jest-cli": "^27.3.1" - } - }, - "jest-changed-files": { - "version": "27.3.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.3.0.tgz", - "integrity": "sha512-9DJs9garMHv4RhylUMZgbdCJ3+jHSkpL9aaVKp13xtXAD80qLTLrqcDZL1PHA9dYA0bCI86Nv2BhkLpLhrBcPg==", - "dev": true, - "requires": { - "@jest/types": "^27.2.5", - "execa": "^5.0.0", - "throat": "^6.0.1" }, "dependencies": { "@jest/types": { @@ -12407,6 +12396,40 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-cli": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.3.1.tgz", + "integrity": "sha512-WHnCqpfK+6EvT62me6WVs8NhtbjAS4/6vZJnk7/2+oOr50cwAzG4Wxt6RXX0hu6m1169ZGMlhYYUNeKBXCph/Q==", + "dev": true, + "requires": { + "@jest/core": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "jest-config": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + } + }, + "jest-util": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.3.1.tgz", + "integrity": "sha512-8fg+ifEH3GDryLQf/eKZck1DEs2YuVPBCMOaHQxVVLmQwl/CDhWzrvChTX4efLZxGrw+AA0mSXv78cyytBt/uw==", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.4", + "picomatch": "^2.2.3" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12418,30 +12441,14 @@ } } }, - "jest-circus": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.3.1.tgz", - "integrity": "sha512-v1dsM9II6gvXokgqq6Yh2jHCpfg7ZqV4jWY66u7npz24JnhP3NHxI0sKT7+ZMQ7IrOWHYAaeEllOySbDbWsiXw==", + "jest-changed-files": { + "version": "27.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.3.0.tgz", + "integrity": "sha512-9DJs9garMHv4RhylUMZgbdCJ3+jHSkpL9aaVKp13xtXAD80qLTLrqcDZL1PHA9dYA0bCI86Nv2BhkLpLhrBcPg==", "dev": true, "requires": { - "@jest/environment": "^27.3.1", - "@jest/test-result": "^27.3.1", "@jest/types": "^27.2.5", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.3.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.3.1", - "jest-matcher-utils": "^27.3.1", - "jest-message-util": "^27.3.1", - "jest-runtime": "^27.3.1", - "jest-snapshot": "^27.3.1", - "jest-util": "^27.3.1", - "pretty-format": "^27.3.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", + "execa": "^5.0.0", "throat": "^6.0.1" }, "dependencies": { @@ -12492,40 +12499,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.3.1.tgz", - "integrity": "sha512-8fg+ifEH3GDryLQf/eKZck1DEs2YuVPBCMOaHQxVVLmQwl/CDhWzrvChTX4efLZxGrw+AA0mSXv78cyytBt/uw==", - "dev": true, - "requires": { - "@jest/types": "^27.2.5", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.4", - "picomatch": "^2.2.3" - } - }, - "pretty-format": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz", - "integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==", - "dev": true, - "requires": { - "@jest/types": "^27.2.5", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12537,24 +12510,31 @@ } } }, - "jest-cli": { + "jest-circus": { "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.3.1.tgz", - "integrity": "sha512-WHnCqpfK+6EvT62me6WVs8NhtbjAS4/6vZJnk7/2+oOr50cwAzG4Wxt6RXX0hu6m1169ZGMlhYYUNeKBXCph/Q==", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.3.1.tgz", + "integrity": "sha512-v1dsM9II6gvXokgqq6Yh2jHCpfg7ZqV4jWY66u7npz24JnhP3NHxI0sKT7+ZMQ7IrOWHYAaeEllOySbDbWsiXw==", "dev": true, "requires": { - "@jest/core": "^27.3.1", + "@jest/environment": "^27.3.1", "@jest/test-result": "^27.3.1", "@jest/types": "^27.2.5", + "@types/node": "*", "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "jest-config": "^27.3.1", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.3.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", "jest-util": "^27.3.1", - "jest-validate": "^27.3.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" + "pretty-format": "^27.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" }, "dependencies": { "@jest/types": { @@ -12618,6 +12598,26 @@ "picomatch": "^2.2.3" } }, + "pretty-format": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz", + "integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14507,9 +14507,9 @@ } }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "dev": true }, "chalk": { diff --git a/src/App.svelte b/src/App.svelte index 2417d323..2b798f26 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -8,8 +8,18 @@ import ByCategory from "./routes/ByCategory.svelte"; import CensusCategories from "./routes/CensusCategories.svelte"; import { Router, Link, Route } from "svelte-routing"; + import { initialiseGeography } from "./model/geography/geography"; + import LegacyGeographyService from "./model/geography/services/legacyGeographyService"; + import { initialiseCensusData } from "./model/censusdata/censusdata"; + import LegacyCensusDataService from "./model/censusdata/services/legacyCensusDataService"; + import { setInitialised } from "./model/appstate"; export let url = ""; + + initialiseGeography(new LegacyGeographyService()).then(() => { + initialiseCensusData(new LegacyCensusDataService()); + setInitialised(); + }); diff --git a/src/MapComponent.svelte b/src/MapComponent.svelte index 5188cfeb..565babb4 100644 --- a/src/MapComponent.svelte +++ b/src/MapComponent.svelte @@ -45,7 +45,6 @@ map.addControl(new NavigationControl()); - // bounds.subscribe((b) => { if (map) map.fitBounds(b, { padding: 20 }); })// move map on bbox change map.fitBounds($bounds, { padding: 20 }); // Get initial zoom level diff --git a/src/MapLayer.svelte b/src/MapLayer.svelte index 70796c65..d0809676 100644 --- a/src/MapLayer.svelte +++ b/src/MapLayer.svelte @@ -41,10 +41,12 @@ let selectedPrev = null; let highlightedPrev = null; + // remove map if present if (map.getLayer(id)) { map.removeLayer(id); } + // map options let options = { id: id, type: type, @@ -70,8 +72,6 @@ map.addLayer(options, order); function updateData() { - console.log("updating colours..."); - data.lsoa.data.forEach((d) => { map.setFeatureState( { @@ -86,6 +86,7 @@ }); } + // when data updates colourise the map $: data && updateData(); $: if (click && selected != selectedPrev) { diff --git a/src/MapSource.svelte b/src/MapSource.svelte index 84d948fc..1ef886a76 100644 --- a/src/MapSource.svelte +++ b/src/MapSource.svelte @@ -16,28 +16,28 @@ const { getMap } = getContext("map"); const map = getMap(); + // clears out self on map object if (map.getSource(id)) { map.removeSource(id); } + // watches for isSourceLoaded method on map function isSourceLoaded() { if (map.isSourceLoaded(id)) { loaded = true; - console.log(id + " loaded!"); } else { setTimeout(() => { - console.log("..."); isSourceLoaded(); }, 500); } } + // watches for isMapLoaded then runs the addSource method function isMapLoaded() { if (map.isStyleLoaded(id)) { addSource(); } else { setTimeout(() => { - console.log("..."); isMapLoaded(); }, 500); } @@ -57,9 +57,9 @@ props.promoteId = promoteId; } + // runs the addSource method function addSource() { - console.log(id + " map source loading..."); - if (type == "geojson") { + if (type === "geojson") { if (data) { map.addSource(id, { type: type, @@ -75,7 +75,7 @@ }); isSourceLoaded(); } - } else if (type == "vector") { + } else if (type === "vector") { map.addSource(id, { type: type, tiles: [url], @@ -85,6 +85,7 @@ } } + // kicks off the chain isMapLoaded(); diff --git a/src/config.js b/src/config.js new file mode 100644 index 00000000..d8030174 --- /dev/null +++ b/src/config.js @@ -0,0 +1,72 @@ +export default { + legacy: { + geography: "TYPE298", + mapstyle: "https://bothness.github.io/ons-basemaps/data/style-omt.json", + tabledata: "https://bothness.github.io/census-atlas/data/indicators.json", + ladtopo: { + url: "https://bothness.github.io/census-atlas/data/lad_boundaries_2020.json", + layer: "LA2020EW", + code: "AREACD", + name: "AREANM", + }, + lsoabldg: { + url: "https://cdn.ons.gov.uk/maptiles/buildings/v1/{z}/{x}/{y}.pbf", + layer: "buildings", + code: "lsoa11cd", + }, + lsoabounds: { + url: "https://cdn.ons.gov.uk/maptiles/administrative/lsoa/v2/boundaries/{z}/{x}/{y}.pbf", + layer: "lsoa", + code: "areacd", + }, + ladvector: { + url: "https://cdn.ons.gov.uk/maptiles/administrative/authorities/v1/boundaries/{z}/{x}/{y}.pbf", + layer: "authority", + code: "areacd", + }, + lsoadata: "https://bothness.github.io/census-atlas/data/lsoa2011_lad2020.csv", + }, + ux: { + legend_sections: 5, + legend_colours: ["#d5f690", "#5bc4b1", "#2e9daa", "#0079a2", "#005583"], + map: { + default_zoom: 14, + max_zoom: 14, + min_zoom: 9, + buildings_breakpoint: 12, + lsoa_breakpoint: 9, + paint: { + data: { + "fill-color": [ + "case", + ["!=", ["feature-state", "color"], null], + ["feature-state", "color"], + "rgba(255, 255, 255, 0)", + ], + }, + line: { + "line-color": ["rgba(192, 192, 192, 1)"], + "line-width": [0.75], + }, + interactive: { + "line-color": [ + "case", + ["==", ["feature-state", "selected"], true], + "rgba(0, 0, 0, 1)", + ["==", ["feature-state", "hovered"], true], + "rgba(50, 50, 50, 1)", + "rgba(0, 0, 0, 0)", + ], + "line-width": [ + "case", + ["==", ["feature-state", "selected"], true], + 2, + ["==", ["feature-state", "hovered"], true], + 2, + 0, + ], + }, + }, + }, + }, +}; diff --git a/src/data/test/mockDSData.js b/src/data/test/mockDSData.js index 56fef848..cfa0a421 100644 --- a/src/data/test/mockDSData.js +++ b/src/data/test/mockDSData.js @@ -1,22 +1,22 @@ -let ethnicity=[ -{id:"black",value:"black",label:"Black"}, -{id:"mixed",value:"mixed",label:"Mixed"}, -{id:"white",value:"white",label:"White"}, -{id:"other",value:"other",label:"Other"}, -{id:"asian",value:"asian",label:"Asian"}, -] +let ethnicity = [ + { id: "black", value: "black", label: "Black" }, + { id: "mixed", value: "mixed", label: "Mixed" }, + { id: "white", value: "white", label: "White" }, + { id: "other", value: "other", label: "Other" }, + { id: "asian", value: "asian", label: "Asian" }, +]; -let cities=[ - { value: "none", label: "Please select", disabled:true}, +let cities = [ + { value: "none", label: "Please select", disabled: true }, { value: "london", label: "London" }, { value: "bristol", label: "Bristol" }, { value: "oxford", label: "Oxford" }, -] +]; -let checkboxData=[ - {id:"laptop", value:"laptop", label:"Laptop"}, - {id:"mobile", value:"mobile", label:"Mobile"}, - {id:"tablet", value:"tablet", label:"Tablet"}, -] +let checkboxData = [ + { id: "laptop", value: "laptop", label: "Laptop" }, + { id: "mobile", value: "mobile", label: "Mobile" }, + { id: "tablet", value: "tablet", label: "Tablet" }, +]; -export {ethnicity, cities, checkboxData} +export { ethnicity, cities, checkboxData }; diff --git a/src/dataService.js b/src/dataService.js index 11b3bf1d..5a002e5d 100644 --- a/src/dataService.js +++ b/src/dataService.js @@ -1,41 +1,36 @@ import { csvParse } from "d3-dsv"; import { get } from "svelte/store"; -export default function LocalDataService() {} +export default class LocalDataService { + getGeographicCodes = async function (url) { + let response = await fetch(url); + let string = await response.text(); + return await csvParse(string, (d) => { + return d["GEOGRAPHY_CODE"]; + }); + }; -LocalDataService.prototype.getGeographicCodes = async function (url) { - let response = await fetch(url); - let string = await response.text(); - return await csvParse(string, (d) => { - return d["GEOGRAPHY_CODE"]; - }); -}; + getCategoryTotals = async function (url) { + let response = await fetch(url); + let string = await response.text(); + let data = await csvParse(string, (d) => { + return d["0"]; + }); + return data; + }; -LocalDataService.prototype.getCategoryTotals = async function (url) { - let response = await fetch(url); - let string = await response.text(); - let data = await csvParse(string, (d) => { - return d["0"]; - }); - return data; -}; - -LocalDataService.prototype.getNomisData = async function ( - url, - geographicCodesStore, - selectedCategoryTotals, - indicatorCode, -) { - let response = await fetch(url); - let string = await response.text(); - let geoCodes = get(geographicCodesStore); - let categoryTotals = get(selectedCategoryTotals); - return await csvParse(string, (d, index) => { - return { - code: geoCodes[index], - value: +d[indicatorCode], - count: +categoryTotals[index], - perc: (+d[indicatorCode] / +categoryTotals[index]) * 100, - }; - }); -}; + getNomisData = async function (url, geographicCodesStore, selectedCategoryTotals, indicatorCode) { + let response = await fetch(url); + let string = await response.text(); + let geoCodes = get(geographicCodesStore); + let categoryTotals = get(selectedCategoryTotals); + return await csvParse(string, (d, index) => { + return { + code: geoCodes[index], + value: +d[indicatorCode], + count: +categoryTotals[index], + perc: (+d[indicatorCode] / +categoryTotals[index]) * 100, + }; + }); + }; +} diff --git a/src/model/appstate.js b/src/model/appstate.js new file mode 100644 index 00000000..a20c3b68 --- /dev/null +++ b/src/model/appstate.js @@ -0,0 +1,7 @@ +import { writable } from "svelte/store"; + +export let appIsInitialised = writable(false); + +export const setInitialised = () => { + appIsInitialised.set(true); +}; diff --git a/src/model/censusdata/censusdata.js b/src/model/censusdata/censusdata.js new file mode 100644 index 00000000..afbe157e --- /dev/null +++ b/src/model/censusdata/censusdata.js @@ -0,0 +1,26 @@ +import { writable } from "svelte/store"; + +export let categoryDataIsLoaded = writable(false); +export let tableIsLoaded = writable(false); + +export let categoryData = {}; +export let tableData = {}; +export let breaks = []; + +let dataService = null; + +export function initialiseCensusData(censusDataService) { + dataService = censusDataService; +} + +export async function fetchCensusData(categoryCode, geographyCode) { + categoryDataIsLoaded.set(false); + + // Do a simple data load + let lsoaData = await dataService.fetchLsoaCategoryData(categoryCode); + let higherData = await dataService.fetchHigherGeographyCategoryData(categoryCode); + + categoryData = { ...lsoaData, ...higherData }; + breaks = await dataService.fetchLegendBreakpoints(categoryCode); + categoryDataIsLoaded.set(true); +} diff --git a/src/stores/indicators.js b/src/model/censusdata/censusdata.test.js similarity index 100% rename from src/stores/indicators.js rename to src/model/censusdata/censusdata.test.js diff --git a/src/model/censusdata/services/legacyCensusDataService.js b/src/model/censusdata/services/legacyCensusDataService.js new file mode 100644 index 00000000..e2fbfe63 --- /dev/null +++ b/src/model/censusdata/services/legacyCensusDataService.js @@ -0,0 +1,153 @@ +import { csvParse } from "d3-dsv"; +import { ckmeans } from "simple-statistics"; +import { lsoaLookup } from "../../geography/geography"; +import config from "../../../config"; + +export default class LegacyCensusDataService { + constructor() { + this.reset(); + } + + reset() { + this.dataset = { + lsoa: { + data: [], + index: {}, + breaks: [], + }, + higher: { + data: [], + index: {}, + }, + lad: { + data: [], + index: {}, + }, + englandAndWales: { + count: 0, + value: 0, + }, + }; + } + + async fetchLsoaCategoryData(categoryId) { + let url = `https://bothness.github.io/census-atlas/data/lsoa/${categoryId}.csv`; + let response = await fetch(url); + let string = await response.text(); + let category = this._getCategory(categoryId); + + this.reset(); + this.dataset.lsoa.data = csvParse(string, (d, index) => { + return { + code: d["GEOGRAPHY_CODE"], + value: +d[category.cell], + count: +d[0], + perc: (+d[category.cell] / +d[0]) * 100, + }; + }); + + this._processLegacyDataset(); + + return this.dataset.lsoa.index; + } + + async fetchLegendBreakpoints(categoryId) { + return this.dataset.lsoa.breaks; + } + + async fetchHigherGeographyCategoryData(categoryId) { + // is derived in legacy version from + return this.dataset.higher.index; + } + + async fetchCategoryAggregateData(categoryId) { + return { + breaks: this.dataset.lsoa.breaks, + }; + } + + async fetchTableForGeography(tableId, geographyId) { + return { + tableId: tableId, + geographyId: geographyId, + rows: [ + // { category: 'Female', value: 4801, perc: 0.49 } + ], + }; + } + + // --------------------------------- + + _processLegacyDataset() { + this.dataset.lsoa.data.sort((a, b) => a.perc - b.perc); + + let vals = this.dataset.lsoa.data.map((d) => d.perc); + let chunks = ckmeans(vals, config.ux.legend_sections); + this.dataset.lsoa.breaks = this._getBreaks(chunks); + + for (const lsoa of this.dataset.lsoa.data) { + this.dataset.lsoa.index[lsoa.code] = lsoa; + } + + // aggregate lad data + for (const lsoa of this.dataset.lsoa.data) { + let ladCode = lsoaLookup[lsoa.code].parent; + if (!this.dataset.lad.index[ladCode]) { + this.dataset.lad.index[ladCode] = { + code: ladCode, + value: 0, + count: 0, + }; + } + this.dataset.lad.index[ladCode].value += lsoa.value; + this.dataset.lad.index[ladCode].count += lsoa.count; + } + + for (const ladCode of Object.keys(this.dataset.lad.index)) { + this.dataset.lad.index[ladCode].perc = + (this.dataset.lad.index[ladCode].value / this.dataset.lad.index[ladCode].count) * 100; + this.dataset.lad.data.push(this.dataset.lad.index[ladCode]); + } + + this.dataset.lad.data.sort((a, b) => a.perc - b.perc); + + // aggregate national data + for (const lsoa of this.dataset.lsoa.data) { + this.dataset.englandAndWales.value += lsoa.value; + this.dataset.englandAndWales.count += lsoa.count; + } + this.dataset.englandAndWales.perc = (this.dataset.englandAndWales.value / this.dataset.englandAndWales.count) * 100; + + // + this.dataset.higher = this.dataset.lad; + this.dataset.higher.index["ENGLAND_AND_WALES"] = this.dataset.englandAndWales; + this.dataset.higher.data.push(this.dataset.englandAndWales); + + this.dataset.englandAndWales = { + count: 0, + value: 0, + }; + this.dataset.lad = { + data: [], + index: {}, + }; + } + + _getBreaks(chunks) { + let breaks = []; + + chunks.forEach((chunk) => { + breaks.push(chunk[0]); + }); + + breaks.push(chunks[chunks.length - 1][chunks[chunks.length - 1].length - 1]); + return breaks; + } + + _getCategory(categoryCode) { + return { + code: categoryCode, + cell: +categoryCode.slice(7, 10), + }; + } +} diff --git a/src/stores/geography.js b/src/model/geography/geography.js similarity index 53% rename from src/stores/geography.js rename to src/model/geography/geography.js index c4f1753e..3a402471 100644 --- a/src/stores/geography.js +++ b/src/model/geography/geography.js @@ -1,12 +1,79 @@ +import { writable } from "svelte/store"; +import config from "../../config"; + +// CONSTANTS +// initialised below export var ladBoundaries = []; export var ladList = []; export var ladLookup = {}; export var lsoaLookup = {}; +// WRITABLES +// reactive variables that can be subscribed to in our Svelte files +export let loadingGeography = writable(false); +export let selectedGeography = writable({ + lad: null, + lsoa: null, +}); +export let hoveredGeography = writable({ + lad: null, + lsoa: null, +}); +export let zoom = writable(config.ux.default_zoom); + +// ACTIONS +export function updateSelectedGeography(geographyCode) { + selectedGeography.set(getLadAndLsoa(geographyCode)); +} + +export function updateHoveredGeography(geographyCode) { + hoveredGeography.set(getLadAndLsoa(geographyCode)); +} + +export function updateZoom(newZoom) { + zoom.set(newZoom); +} + +// ------ + +function getLadAndLsoa(geographyCode) { + if (ladLookup[geographyCode] === null) { + return { + lad: lsoaLookup[geographyCode].parent, + lsoa: geographyCode, + }; + } else { + return { + lad: geographyCode, + lsoa: null, + }; + } +} + +// RESET (for tests) + +export function reset() { + ladBoundaries = []; + ladList = []; + ladLookup = {}; + lsoaLookup = {}; + loadingGeography.set(false); + selectedGeography.set({ + lad: null, + lsoa: null, + }); + hoveredGeography.set({ + lad: null, + lsoa: null, + }); +} + +// INITIALISERS const LAD_AREA_CODE = "AREACD"; const LAD_AREA_NAME = "AREANM"; export async function initialiseGeography(geographyService) { + loadingGeography.set(true); ladBoundaries = await geographyService.getLadBoundaries(); let lsoaData = await geographyService.getLsoaData(); @@ -14,14 +81,7 @@ export async function initialiseGeography(geographyService) { lsoaLookup = buildLsoaLookup(lsoaData); ladList = buildLadList(ladBoundaries, ladLookup); - return { ladBoundaries, ladLookup, lsoaLookup, ladList }; -} - -export function reset() { - ladBoundaries = []; - ladList = []; - ladLookup = {}; - lsoaLookup = {}; + loadingGeography.set(false); } function buildLadList(ladBounds, ladLookup) { @@ -50,6 +110,8 @@ function buildLadLookup(ladBounds, lsoaData) { lookup[d.parent].children.push(d.code); } }); + + return lookup; } function buildLsoaLookup(lsoaData) { diff --git a/src/model/geography/geography.test.js b/src/model/geography/geography.test.js new file mode 100644 index 00000000..bd1349de --- /dev/null +++ b/src/model/geography/geography.test.js @@ -0,0 +1,40 @@ +import MockGeographyService from "./geography/mockGeographyService"; +import { initialiseGeography, loadingGeography, reset } from "./geography"; + +describe("initialise geography", () => { + it("it calls functions from the geography service", async () => { + // given + // a mock for the geography service + const mockGeographyService = new MockGeographyService({ features: [] }, []); + + // when + // we call initialise geography + await initialiseGeography(mockGeographyService); + + // then + // it calls functions on the geography service + expect(mockGeographyService.getLadBoundariesCalled).toBe(1); + expect(mockGeographyService.getLsoaDataCalled).toBe(1); + }); + + it("switches geographyLoading to true and back again", async () => { + // given + // we reset + reset(); + const mockGeographyService = new MockGeographyService({ features: [] }, []); + + // and record a change history for loadingGeography + var changeHistory = []; + loadingGeography.subscribe((value) => { + changeHistory.push(value); + }); + + // when + // we call initialise geography + await initialiseGeography(mockGeographyService); + + // then + // it will have switched loading from false to true and back again + expect(changeHistory).toStrictEqual([false, true, false]); + }); +}); diff --git a/src/model/geography/services/legacyGeographyService.js b/src/model/geography/services/legacyGeographyService.js new file mode 100644 index 00000000..17a52d48 --- /dev/null +++ b/src/model/geography/services/legacyGeographyService.js @@ -0,0 +1,20 @@ +import { autoType, csvParse } from "d3-dsv"; +import { feature } from "topojson-client"; +import config from "../../../config"; + +export default class LegacyGeographyService { + getLadBoundaries = async function () { + let url = config.legacy.ladtopo.url; + let layer = config.legacy.ladtopo.layer; + let response = await fetch(url); + let topojson = await response.json(); + return feature(topojson, layer); + }; + + getLsoaData = async function () { + let url = config.legacy.lsoadata; + let response = await fetch(url); + let string = await response.text(); + return csvParse(string, autoType); + }; +} diff --git a/src/services/mockGeographyService.js b/src/model/geography/services/mockGeographyService.js similarity index 100% rename from src/services/mockGeographyService.js rename to src/model/geography/services/mockGeographyService.js diff --git a/src/model/utils.js b/src/model/utils.js new file mode 100644 index 00000000..27b9e7f8 --- /dev/null +++ b/src/model/utils.js @@ -0,0 +1,8 @@ +export function getLegendSection(value, breakpoints) { + for (let i = 1; i < breakpoints.length; i++) { + if (value <= breakpoints[i]) { + return i; + } + } + return breakpoints.length; +} diff --git a/src/routes/ByCategory.svelte b/src/routes/ByCategory.svelte index 82344dab..1d4c6717 100644 --- a/src/routes/ByCategory.svelte +++ b/src/routes/ByCategory.svelte @@ -1,7 +1,7 @@ @@ -27,7 +35,65 @@ - + + + {#if $categoryDataIsLoaded} + + {/if} + { + updateSelectedGeography(code); + }} + onHover={(code) => { + updateHoveredGeography(code); + }} + /> + + + + {#if $categoryDataIsLoaded} + + {/if} + { + updateSelectedGeography(code); + }} + onHover={(code) => { + updateHoveredGeography(code); + }} + /> + + + {#if $categoryDataIsLoaded} + + {/if} + + @@ -44,8 +110,8 @@ Explore correlations between two indicators in advanced mode. + >Explore correlations between two indicators in advanced mode. + @@ -54,6 +120,7 @@ diff --git a/src/routes/CensusAtlas.svelte b/src/routes/CensusAtlas.svelte index 49a7bfb1..56a8be6a 100644 --- a/src/routes/CensusAtlas.svelte +++ b/src/routes/CensusAtlas.svelte @@ -1,7 +1,6 @@ + code={` {#each ethnicity as option} diff --git a/src/services/flatfileGeographyService.js b/src/services/flatfileGeographyService.js deleted file mode 100644 index c4bf1c42..00000000 --- a/src/services/flatfileGeographyService.js +++ /dev/null @@ -1,18 +0,0 @@ -import { autoType, csvParse } from "d3-dsv"; -import { feature } from "topojson-client"; - -export default function FlatfileGeographyService() {} - -FlatfileGeographyService.prototype.getLadBoundaries = async function () { - let response = await fetch(url); - let topojson = await response.json(); - let geojson = await feature(topojson, layer); - return geojson; -}; - -FlatfileGeographyService.prototype.getLsoaData = async function () { - let response = await fetch(url); - let string = await response.text(); - let data = await csvParse(string, autoType); - return data; -}; diff --git a/src/stores/geography.test.js b/src/stores/geography.test.js deleted file mode 100644 index 964012d6..00000000 --- a/src/stores/geography.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import MockGeographyService from "../services/mockGeographyService"; -import { initialiseGeography } from "./geography"; - -it("it calls functions from the geography service", async () => { - // given - // a mock for the geography service - const mockGeographyService = new MockGeographyService({ features: [] }, []); - - // when - // we call initialise geography - await initialiseGeography(mockGeographyService); - - // then - // it calls functions on the geography service - expect(mockGeographyService.getLadBoundariesCalled).toBe(1); - expect(mockGeographyService.getLsoaDataCalled).toBe(1); -}); diff --git a/src/ui/map/BoundaryLayer.svelte b/src/ui/map/BoundaryLayer.svelte new file mode 100644 index 00000000..412b18d1 --- /dev/null +++ b/src/ui/map/BoundaryLayer.svelte @@ -0,0 +1,47 @@ + diff --git a/src/ui/map/DataLayer.svelte b/src/ui/map/DataLayer.svelte new file mode 100644 index 00000000..61321372 --- /dev/null +++ b/src/ui/map/DataLayer.svelte @@ -0,0 +1,81 @@ + diff --git a/src/ui/map/InteractiveLayer.svelte b/src/ui/map/InteractiveLayer.svelte new file mode 100644 index 00000000..073fb572 --- /dev/null +++ b/src/ui/map/InteractiveLayer.svelte @@ -0,0 +1,163 @@ + diff --git a/src/ui/Map.svelte b/src/ui/map/Map.svelte similarity index 92% rename from src/ui/Map.svelte rename to src/ui/map/Map.svelte index 2d30865e..817660f5 100644 --- a/src/ui/Map.svelte +++ b/src/ui/map/Map.svelte @@ -1,7 +1,7 @@ + +{#if loaded} + +{/if} diff --git a/src/ui/map/map.md b/src/ui/map/map.md new file mode 100644 index 00000000..8870d14b --- /dev/null +++ b/src/ui/map/map.md @@ -0,0 +1,63 @@ +# Map + +The map has been refactored to make it easier to use. There is no html in the MapComponent.svelte components which makes +this relaltively easy + +The intent has been to refactor maps so that as much config as possible is either inherited using `getContext` or picked +up from config + +This makes it relatively easy to work out what is going on with the Map + +## Map 1 + +This is a map which is pulling from one tile set. +`` renders the borders of its parent TileSet. Setting `` lets us customise the boundaries + +```html + + + + + +``` + +## Map 2 + +This map renders two new layers. `` is what renders the data. `` gives two callbacks which we can use to update our datastores. + +```html + + + + + {updateSelectedGeography(code)}} + onHover={(code)=>{updateHoveredGeography(code)}} /> + + +``` + +## Map 3 + +This is where life gets interesting. By setting `minzoom` and `maxzoom` we control when layers and tile sets are visible + +In this example if you are zoomed out the lad level data is rendered. +When you zoom in above 9 the lad layers are hidden and the more detailed +lsoa data becomes active. + +```html + + + + + {updateSelectedGeography(code)}} + onHover={(code)=>{updateHoveredGeography(code)}} /> + + + + + + {updateSelectedGeography(code)}} + onHover={(code)=>{updateHoveredGeography(code)}} /> + + +``` diff --git a/src/utils.js b/src/utils.js index 439b6e63..9a2ab514 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,6 @@ import { feature } from "topojson-client"; import { csvParse, autoType } from "d3-dsv"; import { get } from "svelte/store"; -import { bbox } from "@turf/turf"; import { ckmeans } from "simple-statistics"; export async function getLsoaData(url) { @@ -11,14 +10,8 @@ export async function getLsoaData(url) { return data; } -export async function getTopo(url, layer) { - let response = await fetch(url); - let topojson = await response.json(); - let geojson = await feature(topojson, layer); - return geojson; -} - export async function getNomis(url, dataService, geographicCodesStore, selectedCategoryTotals, indicatorCode) { + console.log(indicatorCode); let geoCodesStore = get(geographicCodesStore); if (geoCodesStore.length == 0) { let geoCodes = await dataService.getGeographicCodes(url); @@ -55,6 +48,7 @@ export function processAggregateData(dataset, lookup) { function calculateAggregateData(lsoaData, lsoa, lookup, lad, ladTemp, englandAndWales) { lsoa.index[lsoaData.code] = lsoaData; let parent = lookup[lsoaData.code].parent; + if (!lad.index[parent]) { lad.index[parent] = { code: parent, @@ -203,14 +197,3 @@ export function updateURL(location, selectCode, active, mapLocation, history) { history.pushState(undefined, undefined, newhash); } } - -// export function replaceURL(selectCode, active, mapLocation, history) { -// let hash = `#/${selectCode}/${active.lad.selected ? active.lad.selected : "" -// }/${active.lsoa.selected ? active.lsoa.selected : ""}/${mapLocation.zoom||14},${mapLocation.lon -// },${mapLocation.lat}`; -// history.replaceState(undefined, undefined, hash); -// } - -export function testFunction() { - return true; -}