diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6d33a35..908f976 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -80,3 +80,49 @@ jobs:
- name: Build
run: npm run build
+
+ lint-rust:
+ name: Lint Rust
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: server-rs
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy, rustfmt
+
+ - name: Cache cargo
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: server-rs
+
+ - name: Run clippy
+ run: cargo clippy -- -D warnings
+
+ - name: Check formatting
+ run: cargo fmt --check
+
+ test-rust:
+ name: Test Rust
+ runs-on: ubuntu-latest
+ needs: [lint-rust]
+ defaults:
+ run:
+ working-directory: server-rs
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: server-rs
+
+ - name: Run tests
+ run: cargo test
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3c9d529..702822d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,8 +3,10 @@
"*.venv": true,
"**/__pycache__": true,
"**/node_modules": true,
- "**/.ruff_cache":true,
- "**/.pytest_cache":true,
- "**/target":true
+ "**/.ruff_cache": true,
+ "**/.pytest_cache": true,
+ "**/target": true,
+ "frontend/dist": true,
+ "**/.task": true
}
-}
+}
\ No newline at end of file
diff --git a/Taskfile.yml b/Taskfile.yml
index 677d189..1e55c2d 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -42,7 +42,7 @@ tasks:
dir: og-screenshot
env:
CACHE_DIR: /tmp/og-cache
- NARROWIT_URL: http://localhost:3030
+ NARROWIT_URL: http://localhost:3000
cmds:
- npm install
- npx playwright install --with-deps chromium
@@ -50,7 +50,7 @@ tasks:
- npm start
dev:frontend:
- desc: Run frontend dev server on port 3030 (proxies /api to :8001)
+ desc: Run frontend dev server on port 3000 (proxies /api to :8001)
dir: frontend
deps:
- install
diff --git a/frontend/src/lib/consts.ts b/frontend/src/lib/consts.ts
index 36b35af..6e5fd89 100644
--- a/frontend/src/lib/consts.ts
+++ b/frontend/src/lib/consts.ts
@@ -35,8 +35,9 @@ export const POSTCODE_ZOOM_THRESHOLD = 15;
export const ZOOM_TO_RESOLUTION_THRESHOLDS = [
{ maxZoom: 7.5, resolution: 5 },
{ maxZoom: 9.5, resolution: 6 },
- { maxZoom: 10.5, resolution: 8 },
- { maxZoom: 12, resolution: 9 },
+ { maxZoom: 10.5, resolution: 7 },
+ { maxZoom: 11.5, resolution: 8 },
+ { maxZoom: 13, resolution: 9 },
{ maxZoom: Infinity, resolution: 10 },
] as const;
@@ -76,33 +77,66 @@ export const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/
export const OSM_ATTRIBUTION = '© OpenStreetMap';
// =============================================================================
-// Crime Category Breakdowns
+// Stacked Chart Groups
// =============================================================================
-/** Component crimes that make up each aggregate crime metric */
-export const CRIME_BREAKDOWNS: Record = {
- 'Serious crime (avg/yr)': [
- 'Violence and sexual offences (avg/yr)',
- 'Robbery (avg/yr)',
- 'Burglary (avg/yr)',
- 'Possession of weapons (avg/yr)',
+export interface StackedChartConfig {
+ /** Display label for the chart */
+ label: string;
+ /** If set, use this feature's stats for the total and info popup. Otherwise sum components. */
+ feature?: string;
+ /** Suffix shown after the total value (e.g. "avg/yr") */
+ unit?: string;
+ /** Feature names that make up the segments */
+ components: string[];
+}
+
+/**
+ * Groups whose features should be collapsed into stacked bar charts.
+ * Keyed by feature group name. Each entry defines one stacked chart.
+ */
+export const STACKED_GROUPS: Record = {
+ Crime: [
+ {
+ label: 'Serious crime',
+ feature: 'Serious crime (avg/yr)',
+ unit: 'avg/yr',
+ components: [
+ 'Violence and sexual offences (avg/yr)',
+ 'Robbery (avg/yr)',
+ 'Burglary (avg/yr)',
+ 'Possession of weapons (avg/yr)',
+ ],
+ },
+ {
+ label: 'Minor crime',
+ feature: 'Minor crime (avg/yr)',
+ unit: 'avg/yr',
+ components: [
+ 'Anti-social behaviour (avg/yr)',
+ 'Criminal damage and arson (avg/yr)',
+ 'Shoplifting (avg/yr)',
+ 'Bicycle theft (avg/yr)',
+ 'Theft from the person (avg/yr)',
+ 'Other theft (avg/yr)',
+ 'Vehicle crime (avg/yr)',
+ 'Public order (avg/yr)',
+ 'Drugs (avg/yr)',
+ 'Other crime (avg/yr)',
+ ],
+ },
],
- 'Minor crime (avg/yr)': [
- 'Anti-social behaviour (avg/yr)',
- 'Criminal damage and arson (avg/yr)',
- 'Shoplifting (avg/yr)',
- 'Bicycle theft (avg/yr)',
- 'Theft from the person (avg/yr)',
- 'Other theft (avg/yr)',
- 'Vehicle crime (avg/yr)',
- 'Public order (avg/yr)',
- 'Drugs (avg/yr)',
- 'Other crime (avg/yr)',
+ Demographics: [
+ {
+ label: 'Ethnic composition',
+ unit: '%',
+ components: ['% White', '% Asian', '% Black', '% Mixed', '% Other'],
+ },
],
};
-/** Colors for crime breakdown segments (designed for 10 distinct categories) */
-export const CRIME_SEGMENT_COLORS = [
+/** Colors for stacked bar segments */
+export const SEGMENT_COLORS = [
'#ef4444', // red-500
'#f97316', // orange-500
'#eab308', // yellow-500