From 9dd6cec895e29768f9eb3093c2e27b05707cdafb Mon Sep 17 00:00:00 2001 From: Moggach Date: Tue, 17 Dec 2024 18:11:14 +0000 Subject: [PATCH 01/22] add None to VisualisationType enum --- nextjs/src/app/reports/[id]/reportContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index 542ee6f06..43f503345 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -7,6 +7,7 @@ import { createContext } from 'react' import { PoliticalTileset } from './politicalTilesets' export enum VisualisationType { + None = 'none', Choropleth = 'choropleth', } From 36dd336060462c45a960bbb6d8b13f9fd872db2f Mon Sep 17 00:00:00 2001 From: Moggach Date: Tue, 17 Dec 2024 18:20:14 +0000 Subject: [PATCH 02/22] add none option to data visualisation type drop down --- .../[id]/(components)/ReportVisualisation.tsx | 194 ++++++++++-------- 1 file changed, 105 insertions(+), 89 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx b/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx index 4c856ea80..4c8cfc93a 100644 --- a/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx +++ b/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx @@ -66,100 +66,116 @@ const ReportVisualisation: React.FC = ({ - {Object.values(VisualisationType).map((type) => ( - - {startCase(type)} - - ))} + {Object.values(VisualisationType) + .filter((type) => type !== VisualisationType.None) + .map((type) => ( + + {startCase(type)} + {type === 'choropleth' && ( + + Colour shading by category + + )} + + ))} + + None + -

- Colour shading by category -

+ {/* Conditionally render selectors if visualisationType is not 'none' */} + {visualisationType !== 'none' && ( + <> + {report.layers.length && ( +
+ +

+ Select which data will populate your {selectedBoundaryLabel} +

+
+ )} - {report.layers.length && ( -
- + updateVisualisationConfig({ + dataSourceField: type as MapLayer['id'], + }) + } + value={dataSourceField} + defaultOpen={!dataSourceField} + required + disabled={isLoading} + > +
- )} - {selectedDataSource?.source.dataType === 'AREA_STATS' && ( -
- -

- Select the field from your data source -

-
+ Select data field + + + {isLoading ? : } + + {!isLoading && ( + + {fieldNames.map((field) => ( + + {field} + + ))} + + )} + +

+ Select the field from your data source +

+ + )} + )} From d596d34efd0e177382e280f83d32f8caf62ddbf8 Mon Sep 17 00:00:00 2001 From: Moggach Date: Tue, 17 Dec 2024 18:21:23 +0000 Subject: [PATCH 03/22] dont show choropleth fills when none data visualisation type is selected --- .../MapLayers/PoliticalChoropleths.tsx | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 074b8ad6a..f991d356f 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -1,7 +1,7 @@ import { AnalyticalAreaType } from '@/__generated__/graphql' import { useLoadedMap } from '@/lib/map' import { useAtom } from 'jotai' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { Layer, Source } from 'react-map-gl' import { addCountByGssToMapboxLayer } from '../../addCountByGssToMapboxLayer' import { @@ -38,11 +38,22 @@ const PoliticalChoropleths: React.FC = ({ ? 'visible' : 'none' + const visualisationType = + report.displayOptions?.dataVisualisation?.visualisationType + const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) useClickOnBoundaryEvents(visibility === 'visible' ? tileset : null) + const [fillVisibility, setFillVisibility] = useState<'choropleth' | 'none'>( + visualisationType === 'none' ? 'none' : 'choropleth' + ) + + useEffect(() => { + setFillVisibility(visualisationType === 'none' ? 'none' : 'choropleth') + }, [visualisationType]) + useEffect(() => { if (visibility === 'none') { setSelectedBoundary(null) @@ -78,17 +89,19 @@ const PoliticalChoropleths: React.FC = ({ url={`mapbox://${tileset.mapboxSourceId}`} promoteId={tileset.promoteId} > + {/* Display fill when visualisation type if not none */} {/* Fill of the boundary */} - + {fillVisibility !== 'none' && ( + + )} {/* Border of the boundary */} Date: Tue, 17 Dec 2024 18:55:37 +0000 Subject: [PATCH 04/22] add showBoundaryNames property to ReportConfig interface --- nextjs/src/app/reports/[id]/reportContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index 542ee6f06..89a180040 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -33,6 +33,7 @@ export interface ReportConfig { showLastElectionData?: boolean showPostcodeLabels?: boolean boundaryOutlines?: AnalyticalAreaType[] + showBoundaryNames?: boolean } } From 520b0738630dd5245a267574b8dbe97863dc694a Mon Sep 17 00:00:00 2001 From: Moggach Date: Tue, 17 Dec 2024 18:56:08 +0000 Subject: [PATCH 05/22] add switch for toggling boundary names to report controls --- .../_ReportConfigLegacyControls.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx b/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx index ebc8362b4..7565ad2aa 100644 --- a/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx +++ b/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx @@ -26,7 +26,12 @@ const ReportConfigLegacyControls: React.FC = () => { report: { organisation, displayOptions: { - display: { showLastElectionData, showMPs, showStreetDetails } = {}, + display: { + showLastElectionData, + showMPs, + showStreetDetails, + showBoundaryNames, + } = {}, }, }, } = useReport() @@ -56,7 +61,6 @@ const ReportConfigLegacyControls: React.FC = () => { }, }) } - return (
@@ -76,6 +80,17 @@ const ReportConfigLegacyControls: React.FC = () => { /> Street details
+
+ { + updateReport({ + displayOptions: { display: { showBoundaryNames } }, + }) + }} + /> + Political boundary names +
From daae79a567bb99e9c52b70b42082c2f3a11b334f Mon Sep 17 00:00:00 2001 From: Moggach Date: Tue, 17 Dec 2024 18:56:56 +0000 Subject: [PATCH 06/22] bring value of showBoundaryNames to choropleths component --- .../[id]/(components)/MapLayers/PoliticalChoropleths.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 074b8ad6a..8063dc958 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -37,8 +37,9 @@ const PoliticalChoropleths: React.FC = ({ report.displayOptions?.dataVisualisation?.boundaryType === boundaryType ? 'visible' : 'none' - const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) + const boundaryNames = report.displayOptions?.display.showBoundaryNames + const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) useClickOnBoundaryEvents(visibility === 'visible' ? tileset : null) From ecad9df4a3c21f27019dd6552df9ad235222b67e Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 13:02:34 +0000 Subject: [PATCH 07/22] add visualisation labels mapping to Visualisation Types and add a showDataVisualisation property to dataVisualisation in ReportConfig --- nextjs/src/app/reports/[id]/reportContext.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index 43f503345..623a3a3f1 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -7,10 +7,13 @@ import { createContext } from 'react' import { PoliticalTileset } from './politicalTilesets' export enum VisualisationType { - None = 'none', Choropleth = 'choropleth', } +export const VisualisationLabels: Record = { + [VisualisationType.Choropleth]: 'Colour shading by category', +} + export enum Palette { Blue = 'blue', } @@ -27,6 +30,7 @@ export interface ReportConfig { palette?: Palette dataSource?: MapLayer['id'] dataSourceField?: string + showDataVisualisation?: Record } display: { showStreetDetails?: boolean @@ -42,6 +46,9 @@ export const defaultReportConfig: ReportConfig = { boundaryType: AnalyticalAreaType.ParliamentaryConstituency_2024, visualisationType: VisualisationType.Choropleth, palette: Palette.Blue, + showDataVisualisation: { + [VisualisationType.Choropleth]: true, // Default to Choropleth + }, }, display: { showStreetDetails: false, @@ -51,7 +58,6 @@ export const defaultReportConfig: ReportConfig = { boundaryOutlines: [AnalyticalAreaType.ParliamentaryConstituency_2024], }, } - interface ReportContextProps { report: MapReportExtended deleteReport: () => void From ef8cfa163864df2570283bd04eb11ba0562b228b Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 13:04:32 +0000 Subject: [PATCH 08/22] add switches in UI for potential Visualisation types and update checkedTypes state with the current checked value --- .../[id]/(components)/ReportVisualisation.tsx | 117 ++++++++++-------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx b/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx index 4c8cfc93a..12818f281 100644 --- a/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx +++ b/nextjs/src/app/reports/[id]/(components)/ReportVisualisation.tsx @@ -8,18 +8,18 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' import { startCase } from 'lodash' -import React from 'react' -import { VisualisationType } from '../reportContext' +import React, { useState } from 'react' +import { VisualisationLabels, VisualisationType } from '../reportContext' import useDataByBoundary from '../useDataByBoundary' import CollapsibleSection from './CollapsibleSection' import { UpdateConfigProps } from './ReportConfiguration' import { useReport } from './ReportProvider' - const ReportVisualisation: React.FC = ({ updateVisualisationConfig, }) => { - const { report } = useReport() + const { report, updateReport } = useReport() const { layers, politicalBoundaries, @@ -31,65 +31,80 @@ const ReportVisualisation: React.FC = ({ boundaryType: dataVisualisation?.boundaryType, }) - const visualisationType = dataVisualisation?.visualisationType + const [checkedTypes, setCheckedTypes] = useState>( + () => + Object.values(VisualisationType).reduce( + (acc, type) => ({ + ...acc, + [type]: type === dataVisualisation?.visualisationType, + }), + {} + ) + ) + + const handleSwitchChange = (type: VisualisationType, checked: boolean) => { + setCheckedTypes((prev) => ({ + ...prev, + [type]: checked, + })) + + updateReport({ + displayOptions: { + ...report.displayOptions, + dataVisualisation: { + ...report.displayOptions.dataVisualisation, + showDataVisualisation: { + ...Object.values(VisualisationType).reduce( + (acc, visType) => { + acc[visType] = + report.displayOptions.dataVisualisation + ?.showDataVisualisation?.[visType] ?? false + return acc + }, + {} as Record + ), + [type]: checked, + }, + visualisationType: checked ? type : undefined, + }, + }, + }) + } const dataSourceId = dataVisualisation?.dataSource const dataSourceField = dataVisualisation?.dataSourceField const selectedDataSource = layers.find((layer) => layer.id === dataSourceId) const selectedBoundaryLabel = politicalBoundaries.find( (boundary) => boundary.boundaryType === dataVisualisation?.boundaryType )?.label - const isLoading = !fieldNames || fieldNames.length === 0 return (
-
- +
Type
+
+ {Object.values(VisualisationType).map((type) => ( +
+ handleSwitchChange(type, checked)} + /> + + {VisualisationLabels[type] && ( + + {VisualisationLabels[type]} + + )} +
+ ))}
- {/* Conditionally render selectors if visualisationType is not 'none' */} - {visualisationType !== 'none' && ( + {checkedTypes['choropleth'] && ( <> {report.layers.length && (
From eec3887f36c6be1f435896db6bd2af3bce68bcf6 Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 13:09:31 +0000 Subject: [PATCH 09/22] bring value of showDataVisualisation into Political Choropleths component and show choropleth fill if it is true --- .../MapLayers/PoliticalChoropleths.tsx | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index f991d356f..dd006438f 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -1,7 +1,7 @@ import { AnalyticalAreaType } from '@/__generated__/graphql' import { useLoadedMap } from '@/lib/map' import { useAtom } from 'jotai' -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import { Layer, Source } from 'react-map-gl' import { addCountByGssToMapboxLayer } from '../../addCountByGssToMapboxLayer' import { @@ -38,28 +38,15 @@ const PoliticalChoropleths: React.FC = ({ ? 'visible' : 'none' - const visualisationType = - report.displayOptions?.dataVisualisation?.visualisationType + const showChoropleth = + report.displayOptions?.dataVisualisation?.showDataVisualisation + ?.choropleth ?? false const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) useClickOnBoundaryEvents(visibility === 'visible' ? tileset : null) - const [fillVisibility, setFillVisibility] = useState<'choropleth' | 'none'>( - visualisationType === 'none' ? 'none' : 'choropleth' - ) - - useEffect(() => { - setFillVisibility(visualisationType === 'none' ? 'none' : 'choropleth') - }, [visualisationType]) - - useEffect(() => { - if (visibility === 'none') { - setSelectedBoundary(null) - } - }, [visibility]) - // When the map is loaded and we have the data, add the data to the boundaries useEffect(() => { if (map.loaded && dataByBoundary) { @@ -89,18 +76,22 @@ const PoliticalChoropleths: React.FC = ({ url={`mapbox://${tileset.mapboxSourceId}`} promoteId={tileset.promoteId} > - {/* Display fill when visualisation type if not none */} {/* Fill of the boundary */} - {fillVisibility !== 'none' && ( - + {showChoropleth && ( + <> + + )} {/* Border of the boundary */} = ({ type="line" paint={getSelectedChoroplethEdge()} filter={['==', ['get', tileset.promoteId], selectedBoundary]} - layout={{ visibility, 'line-join': 'round', 'line-round-limit': 0.1 }} + layout={{ + visibility, + 'line-join': 'round', + 'line-round-limit': 0.1, + }} /> = ({ 'interpolate', ['exponential', 1], ['zoom'], - // 7.5, 0, - // 7.8, 1, ], @@ -169,10 +162,8 @@ const PoliticalChoropleths: React.FC = ({ 'interpolate', ['exponential', 1], ['zoom'], - // 7.5, 0, - // 7.8, 1, ], From 265d5e76918ae80c4da5575f68594b9b082c34b7 Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 14:52:53 +0000 Subject: [PATCH 10/22] only show boundary names layer if showBoundaryNames is true --- .../MapLayers/PoliticalChoropleths.tsx | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 8063dc958..3ceb36e42 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -38,8 +38,8 @@ const PoliticalChoropleths: React.FC = ({ ? 'visible' : 'none' const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) - const boundaryNames = report.displayOptions?.display.showBoundaryNames - + const showBoundaryNames = report.displayOptions?.display.showBoundaryNames + console.log('show boundary names', showBoundaryNames) const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) useClickOnBoundaryEvents(visibility === 'visible' ? tileset : null) @@ -144,30 +144,32 @@ const PoliticalChoropleths: React.FC = ({ 'text-halo-width': 1.5, }} /> - + {showBoundaryNames && ( + + )} ) From 9ba6c6e043c721245eadbc45e56838e9a3b5fcc5 Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 15:23:41 +0000 Subject: [PATCH 11/22] scale down area count and label font sizes --- nextjs/src/app/reports/[id]/mapboxStyles.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nextjs/src/app/reports/[id]/mapboxStyles.ts b/nextjs/src/app/reports/[id]/mapboxStyles.ts index 0364bd603..90b683c4b 100644 --- a/nextjs/src/app/reports/[id]/mapboxStyles.ts +++ b/nextjs/src/app/reports/[id]/mapboxStyles.ts @@ -179,14 +179,14 @@ export const getAreaCountLayout = ( 1, [ 'max', - ['*', ['/', ['get', 'count'], max], textScale(max) * 9], - textScale(min) * 10, + ['*', ['/', ['get', 'count'], max], textScale(max) * 5], + textScale(min) * 6, ], 12, [ 'max', - ['*', ['/', ['get', 'count'], max], textScale(max) * 18], - textScale(min) * 20, + ['*', ['/', ['get', 'count'], max], textScale(max) * 14], + textScale(min) * 16, ], ], 'symbol-placement': 'point', @@ -220,14 +220,14 @@ export const getAreaLabelLayout = ( 1, [ 'max', - ['*', ['/', ['get', 'count'], max], textScale(max) * 9], - textScale(min) * 10, + ['*', ['/', ['get', 'count'], max], textScale(max) * 5], + textScale(min) * 6, ], 12, [ 'max', - ['*', ['/', ['get', 'count'], max], textScale(max) * 18], - textScale(min) * 20, + ['*', ['/', ['get', 'count'], max], textScale(max) * 14], + textScale(min) * 16, ], ], 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], From 74448e62fb188266b4cae18c507effedcb5b0a4f Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 15:26:22 +0000 Subject: [PATCH 12/22] add show political boundary names check around area counts as well as labels --- .../MapLayers/PoliticalChoropleths.tsx | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 3ceb36e42..c2b51d34b 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -115,36 +115,37 @@ const PoliticalChoropleths: React.FC = ({ layout={{ visibility, 'line-join': 'round', 'line-round-limit': 0.1 }} /> - - - {showBoundaryNames && ( + type="geojson" + data={getAreaGeoJSON(dataByBoundary)} + > + + = ({ 'text-halo-width': 1.5, }} /> - )} - + + )} ) } From 4174bbd742ea5b98e2d0f32d92ab2121f649f4db Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 15:29:40 +0000 Subject: [PATCH 13/22] initialise showBoundaryNames to false --- nextjs/src/app/reports/[id]/reportContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index 89a180040..0f9f034d5 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -33,7 +33,7 @@ export interface ReportConfig { showLastElectionData?: boolean showPostcodeLabels?: boolean boundaryOutlines?: AnalyticalAreaType[] - showBoundaryNames?: boolean + showBoundaryNames?: false } } From cbaac73076d05ef99a41085bba237e3027eb0363 Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 15:50:30 +0000 Subject: [PATCH 14/22] fix type errors - make showBoundaryNames default to false --- .../[id]/(components)/_ReportConfigLegacyControls.tsx | 6 +++--- nextjs/src/app/reports/[id]/reportContext.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx b/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx index 7565ad2aa..f21a94c8f 100644 --- a/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx +++ b/nextjs/src/app/reports/[id]/(components)/_ReportConfigLegacyControls.tsx @@ -82,10 +82,10 @@ const ReportConfigLegacyControls: React.FC = () => {
{ + checked={showBoundaryNames ?? false} + onCheckedChange={(checked: boolean) => { updateReport({ - displayOptions: { display: { showBoundaryNames } }, + displayOptions: { display: { showBoundaryNames: checked } }, }) }} /> diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index 0f9f034d5..2d25efa83 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -33,7 +33,7 @@ export interface ReportConfig { showLastElectionData?: boolean showPostcodeLabels?: boolean boundaryOutlines?: AnalyticalAreaType[] - showBoundaryNames?: false + showBoundaryNames?: boolean } } @@ -49,6 +49,7 @@ export const defaultReportConfig: ReportConfig = { showMPs: false, showLastElectionData: false, boundaryOutlines: [AnalyticalAreaType.ParliamentaryConstituency_2024], + showBoundaryNames: false, }, } From 186db03191b07ee09ca3ec10514d5d5b3c488b67 Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 15:55:40 +0000 Subject: [PATCH 15/22] combine visualisation check with existing boundary type check --- .../MapLayers/PoliticalChoropleths.tsx | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index dd006438f..6017e82ba 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -32,16 +32,13 @@ const PoliticalChoropleths: React.FC = ({ boundaryType, tileset, }) => { - // Show the layer only if the report is set to show the boundary type + // Show the layer only if the report is set to show the boundary type and the VisualisationType is choropleth const visibility = - report.displayOptions?.dataVisualisation?.boundaryType === boundaryType + report.displayOptions?.dataVisualisation?.boundaryType === boundaryType && + report.displayOptions?.dataVisualisation?.showDataVisualisation?.choropleth ? 'visible' : 'none' - const showChoropleth = - report.displayOptions?.dataVisualisation?.showDataVisualisation - ?.choropleth ?? false - const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) @@ -77,22 +74,19 @@ const PoliticalChoropleths: React.FC = ({ promoteId={tileset.promoteId} > {/* Fill of the boundary */} - {showChoropleth && ( - <> - - - )} + + <> + + + {/* Border of the boundary */} Date: Wed, 18 Dec 2024 16:00:33 +0000 Subject: [PATCH 16/22] remove console log --- .../reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index c2b51d34b..227f429b4 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -39,7 +39,6 @@ const PoliticalChoropleths: React.FC = ({ : 'none' const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) const showBoundaryNames = report.displayOptions?.display.showBoundaryNames - console.log('show boundary names', showBoundaryNames) const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) useClickOnBoundaryEvents(visibility === 'visible' ? tileset : null) From 4f1e6ec2332ab8fb803d2ad4002d402f1a14398b Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 17:34:04 +0000 Subject: [PATCH 17/22] make showBoundaryNames true by default --- nextjs/src/app/reports/[id]/reportContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index 2d25efa83..32f3e52c8 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -49,7 +49,7 @@ export const defaultReportConfig: ReportConfig = { showMPs: false, showLastElectionData: false, boundaryOutlines: [AnalyticalAreaType.ParliamentaryConstituency_2024], - showBoundaryNames: false, + showBoundaryNames: true, }, } From 06583e465fa8c014f855d264e6f30a76fd7f4557 Mon Sep 17 00:00:00 2001 From: Moggach Date: Wed, 18 Dec 2024 17:44:15 +0000 Subject: [PATCH 18/22] combine area count and label visbility check with existing visiblity check and use existing logic on layer layout property to determine whether to show layer --- .../MapLayers/PoliticalChoropleths.tsx | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 227f429b4..d389cf560 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -38,7 +38,11 @@ const PoliticalChoropleths: React.FC = ({ ? 'visible' : 'none' const { data: dataByBoundary } = useDataByBoundary({ report, boundaryType }) - const showBoundaryNames = report.displayOptions?.display.showBoundaryNames + + const boundaryNameVisibility = + visibility === 'visible' && report.displayOptions?.display.showBoundaryNames + ? 'visible' + : 'none' const map = useLoadedMap() const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) useClickOnBoundaryEvents(visibility === 'visible' ? tileset : null) @@ -114,63 +118,62 @@ const PoliticalChoropleths: React.FC = ({ layout={{ visibility, 'line-join': 'round', 'line-round-limit': 0.1 }} /> - {showBoundaryNames && ( - + - + type="symbol" + layout={{ + ...getAreaCountLayout(dataByBoundary), + visibility: boundaryNameVisibility, + }} + paint={{ + 'text-opacity': [ + 'interpolate', + ['exponential', 1], + ['zoom'], + // + 7.5, + 0, + // + 7.8, + 1, + ], + 'text-color': 'white', + 'text-halo-color': '#24262b', + 'text-halo-width': 1.5, + }} + /> - - - )} + + ) } From 3b7c3ffbffa45903a0ff6318d39ca3ab6f7c99aa Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Thu, 19 Dec 2024 02:25:28 +0100 Subject: [PATCH 19/22] feat: dont create actionnetwork rows in test_sources as they cant be deleted --- hub/graphql/types/model_types.py | 10 +- hub/models.py | 3 +- hub/parsons/action_network/action_network.py | 6 +- hub/tests/test_sources.py | 313 +++++++++++++------ 4 files changed, 226 insertions(+), 106 deletions(-) diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index 12656fcbc..be09f0b03 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -593,21 +593,17 @@ async def gss_area(self, info: Info) -> Optional[Area]: @strawberry_django.type(models.GenericData, filters=CommonDataFilter) class GroupedData: label: Optional[str] - # Provide area_type if gss code is not unique (e.g. WMC and WMC23 constituencies) - area_type: Optional[str] = None + # Provide filter if gss code is not unique (e.g. WMC and WMC23 constituencies) + area_type_filter: Optional["AreaTypeFilter"] = None gss: Optional[str] area_data: Optional[strawberry.Private[Area]] = None imported_data: Optional[JSON] = None - area_type_filter: Optional["AreaTypeFilter"] = None @strawberry_django.field async def gss_area(self, info: Info) -> Optional[Area]: if self.area_data is not None: return self.area_data - if self.area_type is not None: - filters = {"area_type__code": self.area_type} - else: - filters = {} + filters = self.area_type_filter.query_filter if self.area_type_filter else {} loader = FieldDataLoaderFactory.get_loader_class( models.Area, field="gss", filters=filters ) diff --git a/hub/models.py b/hub/models.py index 5576ed5dd..1c7f91f6f 100644 --- a/hub/models.py +++ b/hub/models.py @@ -2830,7 +2830,8 @@ def get_record_id(self, record): return record["id"] def get_record_field(self, record, field, field_type=None): - d = record["fields"].get(str(field), None) + record_dict = record["fields"] if "fields" in record else record + d = record_dict.get(str(field), None) if field_type == "image_field" and d is not None and len(d) > 0: # TODO: implement image handling # e.g. [{'id': 'attDWjeMhUfNMTqRG', 'width': 2200, 'height': 1518, 'url': 'https://v5.airtableusercontent.com/v3/u/27/27/1712044800000/CxNHcR-sBRUhrWt_54_NFA/wcYpoqFV5W_wRmVwh2RM8qs-mJkwwHkQLZuhtf7rFk5-34gILMXJeIYg9vQMcTtgSEd1dDb7lU0CrgJldTcZBN9VyaTU0IkYiw1e5PzTs8ZsOEmA6wrva7UavQCnoacL8b7yUt4ZuWWhna8wzZD2MTZC1K1C1wLkfA1UyN76ZDO-Q6WkBjgg5uZv7rtXlhj9/WL6lQJQAHKXqA9J1YIteSJ3J0Yepj69c55PducG607k' diff --git a/hub/parsons/action_network/action_network.py b/hub/parsons/action_network/action_network.py index 8dba67633..baaa770ee 100644 --- a/hub/parsons/action_network/action_network.py +++ b/hub/parsons/action_network/action_network.py @@ -155,14 +155,14 @@ def _get_generator(self, object_name, limit=None, per_page=25, filter=None): while True: response = self._get_page(object_name, page, per_page, filter=filter) page = page + 1 - response_list = response["_embedded"][list(response["_embedded"])[0]] - if not response_list: - return + response_list = response["_embedded"][list(response["_embedded"])[0]] or [] for item in response_list: yield item count = count + 1 if limit and count >= limit: return + if len(response_list) < per_page: + return # Advocacy Campaigns def get_advocacy_campaigns(self, limit=None, per_page=25, page=None, filter=None): diff --git a/hub/tests/test_sources.py b/hub/tests/test_sources.py index d68c6e4b1..93d0c0974 100644 --- a/hub/tests/test_sources.py +++ b/hub/tests/test_sources.py @@ -61,12 +61,12 @@ def tearDown(self) -> None: self.source.teardown_unused_webhooks(force=True) return super().tearDown() - def create_test_record(self, record: models.ExternalDataSource.CUDRecord): + async def create_test_record(self, record: models.ExternalDataSource.CUDRecord): record = self.source.create_one(record) self.records_to_delete.append((self.source.get_record_id(record), self.source)) return record - def create_many_test_records( + async def create_many_test_records( self, records: List[models.ExternalDataSource.CUDRecord] ): records = self.source.create_many(records) @@ -195,7 +195,7 @@ async def test_import_many(self): self.assertEqual(len(df.index), import_count) async def test_fetch_one(self): - record = self.create_test_record( + record = await self.create_test_record( models.ExternalDataSource.CUDRecord( email=f"eh{randint(0, 1000)}sp@gmail.com", postcode="EH99 1SP", @@ -220,16 +220,37 @@ async def test_fetch_one(self): ) async def test_fetch_many(self): - now = str(datetime.now().timestamp()) test_record_data = [ models.ExternalDataSource.CUDRecord( - postcode=now + "11111", email=now + "11111@gmail.com", data={} + postcode="E5 0AA", + email=f"E{randint(0, 1000)}AA@gmail.com", + data=( + { + "addr1": "Millfields Rd", + "city": "London", + "state": "London", + "country": "GB", + } + if isinstance(self.source, models.MailchimpSource) + else {} + ), ), models.ExternalDataSource.CUDRecord( - postcode=now + "22222", email=now + "22222@gmail.com", data={} + postcode="E10 6EF", + email=f"E{randint(0, 1000)}EF@gmail.com", + data=( + { + "addr1": "123 Colchester Rd", + "city": "London", + "state": "London", + "country": "GB", + } + if isinstance(self.source, models.MailchimpSource) + else {} + ), ), ] - records = self.create_many_test_records(test_record_data) + records = await self.create_many_test_records(test_record_data) record_ids = [self.source.get_record_id(record) for record in records] assert len(record_ids) == 2 @@ -254,8 +275,10 @@ async def test_fetch_many(self): for test_record in test_record_data: record = next( filter( - lambda r: self.source.get_record_field(r, self.source.email_field) - == test_record["email"], + lambda r: self.source.get_record_field( + r, self.source.postcode_field + ) + == test_record["postcode"], records, ), None, @@ -263,7 +286,7 @@ async def test_fetch_many(self): self.assertIsNotNone(record) async def test_refresh_one(self): - record = self.create_test_record( + record = await self.create_test_record( models.ExternalDataSource.CUDRecord( email=f"eh{randint(0, 1000)}sp@gmail.com", postcode="EH99 1SP", @@ -315,7 +338,7 @@ async def test_pivot_table(self): [self.custom_data_layer.get_record_id(record) for record in records] ) # Add a test record - record = self.create_test_record( + record = await self.create_test_record( models.ExternalDataSource.CUDRecord( email=f"NE{randint(0, 1000)}DD@gmail.com", postcode="NE12 6DD", @@ -348,16 +371,16 @@ async def test_pivot_table(self): ) async def test_refresh_many(self): - records = self.create_many_test_records( + records = await self.create_many_test_records( [ models.ExternalDataSource.CUDRecord( - postcode="G11 5RD", + postcode="E10 6EF", email=f"gg{randint(0, 1000)}rardd@gmail.com", data=( { - "addr1": "Byres Rd", - "city": "Glasgow", - "state": "Glasgow", + "addr1": "123 Colchester Rd", + "city": "London", + "state": "London", "country": "GB", } if isinstance(self.source, models.MailchimpSource) @@ -365,13 +388,13 @@ async def test_refresh_many(self): ), ), models.ExternalDataSource.CUDRecord( - postcode="G42 8PH", + postcode="E5 0AA", email=f"ag{randint(0, 1000)}rwefw@gmail.com", data=( { - "addr1": "506 Victoria Rd", - "city": "Glasgow", - "state": "Glasgow", + "addr1": "Millfields Rd", + "city": "London", + "state": "London", "country": "GB", } if isinstance(self.source, models.MailchimpSource) @@ -392,29 +415,67 @@ async def test_refresh_many(self): for record in records: if ( self.source.get_record_field(record, self.source.geography_column) - == "G11 5RD" + == "E5 0AA" ): self.assertEqual( self.source.get_record_field(record, self.constituency_field), - "Glasgow West", + "Hackney North and Stoke Newington", ) elif ( self.source.get_record_field(record, self.source.geography_column) - == "G42 8PH" + == "E10 6EF" ): self.assertEqual( self.source.get_record_field(record, self.constituency_field), - "Glasgow South", + "Leyton and Wanstead", ) else: self.fail() - async def test_analytics(self): + async def test_enrichment_electoral_commission(self): + """ + This is testing the ability to enrich data from the data source + using a third party source + """ + # Add a test record + record = await self.create_test_record( + models.ExternalDataSource.CUDRecord( + email=f"NE{randint(0, 1000)}DD@gmail.com", + postcode="DH1 1AE", + data=( + { + "addr1": "38 Swinside Dr", + "city": "Durham", + "state": "Durham", + "country": "GB", + } + if isinstance(self.source, models.MailchimpSource) + else {} + ), + ) + ) + mapped_member = await self.source.map_one( + record, + loaders=await self.source.get_loaders(), + mapping=[ + models.UpdateMapping( + source="electoral_commission_postcode_lookup", + source_path="electoral_services.name", + destination_column="electoral service", + ) + ], + ) + self.assertEqual( + mapped_member["update_fields"]["electoral service"], + "Durham County Council", + ) + + async def test_analytics_counts(self): """ - This is testing the ability to get analytics from the data source + This is testing the ability to get record counts from the data source """ # Add some test data - self.create_many_test_records( + created_records = await self.create_many_test_records( [ models.ExternalDataSource.CUDRecord( postcode="E5 0AA", @@ -447,13 +508,14 @@ async def test_analytics(self): ] ) # import - records = await self.source.fetch_all() + records = await self.source.fetch_many( + [self.source.get_record_id(record) for record in created_records] + ) await self.source.import_many( [self.source.get_record_id(record) for record in records] ) # check analytics analytics = self.source.imported_data_count_by_constituency() - # convert query set to list (is there a better way?) analytics = await sync_to_async(list)(analytics) self.assertGreaterEqual(len(analytics), 2) constituencies_in_report = [a["label"] for a in analytics] @@ -466,6 +528,110 @@ async def test_analytics(self): elif a["label"] == "Leyton and Wanstead": self.assertGreaterEqual(a["count"], 1) + analytics = self.source.imported_data_count_by_area("admin_district") + analytics = await sync_to_async(list)(analytics) + self.assertGreaterEqual(len(analytics), 2) + constituencies_in_report = [a["label"] for a in analytics] + + self.assertIn("Hackney", constituencies_in_report) + self.assertIn("Waltham Forest", constituencies_in_report) + for a in analytics: + if a["label"] == "Hackney": + self.assertGreaterEqual(a["count"], 1) + elif a["label"] == "Waltham Forest": + self.assertGreaterEqual(a["count"], 1) + + async def test_analytics_imported_data(self): + """ + This is testing the ability to get record data from the data source + """ + # Add some test data + created_records = await self.create_many_test_records( + [ + models.ExternalDataSource.CUDRecord( + postcode="E5 0AA", + email=f"E{randint(0, 1000)}AA@gmail.com", + data=( + { + "addr1": "Millfields Rd", + "city": "London", + "state": "London", + "country": "GB", + } + if isinstance(self.source, models.MailchimpSource) + else {} + ), + ), + models.ExternalDataSource.CUDRecord( + postcode="E5 0AB", + email=f"E{randint(0, 1000)}AA@gmail.com", + data=( + { + "addr1": "Millfields Rd", + "city": "London", + "state": "London", + "country": "GB", + } + if isinstance(self.source, models.MailchimpSource) + else {} + ), + ), + models.ExternalDataSource.CUDRecord( + postcode="E10 6EF", + email=f"E{randint(0, 1000)}EF@gmail.com", + data=( + { + "addr1": "123 Colchester Rd", + "city": "London", + "state": "London", + "country": "GB", + } + if isinstance(self.source, models.MailchimpSource) + else {} + ), + ), + ] + ) + # import + records = await self.source.fetch_many( + [self.source.get_record_id(record) for record in created_records] + ) + await self.source.import_many( + [self.source.get_record_id(record) for record in records] + ) + # check analytics + analytics = self.source.imported_data_by_area("parliamentary_constituency") + analytics = await sync_to_async(list)(analytics) + self.assertGreaterEqual(len(analytics), 3) + constituencies_in_report = [a["label"] for a in analytics] + + self.assertIn("Hackney North and Stoke Newington", constituencies_in_report) + self.assertIn("Leyton and Wanstead", constituencies_in_report) + for a in analytics: + postcode = self.source.get_record_field( + a["imported_data"], self.source.postcode_field + ) + if a["label"] == "Hackney North and Stoke Newington": + self.assertIn(postcode, ["E5 0AA", "E5 0AB"]) + elif a["label"] == "Leyton and Wanstead": + self.assertEqual(postcode, "E10 6EF") + + analytics = self.source.imported_data_by_area("admin_district") + analytics = await sync_to_async(list)(analytics) + self.assertGreaterEqual(len(analytics), 3) + constituencies_in_report = [a["label"] for a in analytics] + + self.assertIn("Hackney", constituencies_in_report) + self.assertIn("Waltham Forest", constituencies_in_report) + for a in analytics: + postcode = self.source.get_record_field( + a["imported_data"], self.source.postcode_field + ) + if a["label"] == "Hackney": + self.assertIn(postcode, ["E5 0AA", "E5 0AB"]) + elif a["label"] == "Waltham Forest": + self.assertEqual(postcode, "E10 6EF") + class TestAirtableSource(TestExternalDataSource, TestCase): def create_test_source(self, name="My test Airtable member list"): @@ -496,35 +662,6 @@ def create_test_source(self, name="My test Airtable member list"): ) return self.source - async def test_enrichment_electoral_commission(self): - """ - This is testing the ability to enrich data from the data source - using a third party source - """ - # Add a test record - record = self.create_test_record( - models.ExternalDataSource.CUDRecord( - email=f"NE{randint(0, 1000)}DD@gmail.com", - postcode="DH1 1AE", - data={}, - ) - ) - mapped_member = await self.source.map_one( - record, - loaders=await self.source.get_loaders(), - mapping=[ - models.UpdateMapping( - source="electoral_commission_postcode_lookup", - source_path="electoral_services.name", - destination_column="electoral service", - ) - ], - ) - self.assertEqual( - mapped_member["update_fields"]["electoral service"], - "Durham County Council", - ) - class TestMailchimpSource(TestExternalDataSource, TestCase): constituency_field = "CONSTITUEN" @@ -587,44 +724,32 @@ def create_test_source(self, name="My test AN member list"): ) return self.source + async def create_test_record(self, record: models.ExternalDataSource.CUDRecord): + records = await self.create_many_test_records([record]) + return records[0] + + async def create_many_test_records( + self, records: List[models.ExternalDataSource.CUDRecord] + ): + # don't create records, and return existing records + # this is because Action Network records can't be deleted + postcodes_to_ids = { + "EH99 1SP": "c6d37304-200c-44b4-8eda-04a03e706531", + "NE12 6DD": "2574d845-f5bb-4ba2-af9b-a712d10119b1", + "DH1 1AE": "42fe3b4a-f445-47ce-ba81-7ec38d95dc70", + "E10 6EF": "d88da43f-8984-41d8-80fa-4f9fbb3d6006", + "E5 0AA": "ad6228a2-74c1-48fd-85ee-90eafbaca397", + "E5 0AB": "b762c93b-a23d-45c8-85c4-0d20c3c8a9e5", + } + records = await self.source.fetch_many( + [postcodes_to_ids[record["postcode"]] for record in records] + ) + return records + async def test_fetch_page(self): """ Ensure that fetching page-by-page gives the same count as fetching all. """ - # Add some test data - self.create_many_test_records( - [ - models.ExternalDataSource.CUDRecord( - postcode="E5 0AA", - email=f"E{randint(0, 1000)}AA@gmail.com", - data=( - { - "addr1": "Millfields Rd", - "city": "London", - "state": "London", - "country": "GB", - } - if isinstance(self.source, models.MailchimpSource) - else {} - ), - ), - models.ExternalDataSource.CUDRecord( - postcode="E10 6EF", - email=f"E{randint(0, 1000)}EF@gmail.com", - data=( - { - "addr1": "123 Colchester Rd", - "city": "London", - "state": "London", - "country": "GB", - } - if isinstance(self.source, models.MailchimpSource) - else {} - ), - ), - ] - ) - all_records = await self.source.fetch_all() all_records = list(all_records) paged_records = [] @@ -683,7 +808,7 @@ async def test_fetch_all(self): postcode=now + "22222", email=now + "22222@gmail.com", data={} ), ] - self.create_many_test_records(test_record_data) + await self.create_many_test_records(test_record_data) # Test this functionality records = await self.source.fetch_all() @@ -692,8 +817,6 @@ async def test_fetch_all(self): # Assumes there were 4 records in the test data source before this test ran assert len(records) == 6 - # Check the email field instead of postcode, because Mailchimp doesn't set - # the postcode without a full address, which is not present in this test for test_record in test_record_data: record = next( filter( From e304455d1f71bc7a4bbcf597e38c8bd5fb36137f Mon Sep 17 00:00:00 2001 From: Moggach Date: Thu, 19 Dec 2024 16:06:43 +0000 Subject: [PATCH 20/22] convert membership map markers into circles so that are always at the center of coordinates --- .../(components)/MembersListPointMarkers.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx b/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx index 1d4d9553d..0747669a3 100644 --- a/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx @@ -1,7 +1,7 @@ 'use client' import { BACKEND_URL } from '@/env' -import { layerColour, selectedSourceMarkerAtom, useLoadedMap } from '@/lib/map' +import { selectedSourceMarkerAtom, useLoadedMap } from '@/lib/map' import { useAtom } from 'jotai' import { MapMouseEvent } from 'mapbox-gl' import { useEffect } from 'react' @@ -80,13 +80,10 @@ export function MembersListPointMarkers({ id={`${externalDataSourceId}-marker`} source={externalDataSourceId} source-layer={'generic_data'} - type="symbol" - layout={{ - 'icon-image': `meep-marker-${index}`, - 'icon-allow-overlap': true, - 'icon-ignore-placement': true, - 'icon-size': 0.75, - 'icon-anchor': 'bottom', + type="circle" + paint={{ + 'circle-radius': 8, + 'circle-color': '#678DE3', }} minzoom={MIN_MEMBERS_ZOOM} {...(selectedSourceMarker?.properties?.id @@ -107,8 +104,8 @@ export function MembersListPointMarkers({ source-layer={'generic_data'} type="circle" paint={{ - 'circle-radius': 5, - 'circle-color': layerColour(index, externalDataSourceId), + 'circle-radius': 8, + 'circle-color': '#678DE3', }} minzoom={MIN_MEMBERS_ZOOM} {...(selectedSourceMarker?.properties?.id @@ -129,13 +126,10 @@ export function MembersListPointMarkers({ id={`${externalDataSourceId}-marker-selected`} source={externalDataSourceId} source-layer={'generic_data'} - type="symbol" - layout={{ - 'icon-image': 'meep-marker-selected', - 'icon-size': 0.75, - 'icon-anchor': 'bottom', - 'icon-allow-overlap': true, - 'icon-ignore-placement': true, + type="circle" + paint={{ + 'circle-radius': 10, + 'circle-color': '#678DE3', }} minzoom={MIN_MEMBERS_ZOOM} filter={['==', selectedSourceMarker.properties.id, ['get', 'id']]} From 39dc604ed59a8622f53c0054bb3c45897fdc3c81 Mon Sep 17 00:00:00 2001 From: Moggach Date: Thu, 19 Dec 2024 16:14:42 +0000 Subject: [PATCH 21/22] reduce minimum zoom to see markers --- .../app/reports/[id]/(components)/MembersListPointMarkers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx b/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx index 0747669a3..560d2ec4c 100644 --- a/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx @@ -8,7 +8,7 @@ import { useEffect } from 'react' import { Layer, Source } from 'react-map-gl' import MarkerPopup from './MarkerPopup' import { PLACEHOLDER_LAYER_ID_MARKERS } from './ReportPage' -const MIN_MEMBERS_ZOOM = 10 +const MIN_MEMBERS_ZOOM = 8 export function MembersListPointMarkers({ externalDataSourceId, From 5908fcf89fe4ac9870857c0c2bb1f4f48a453373 Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Tue, 7 Jan 2025 17:19:31 +0100 Subject: [PATCH 22/22] feat: add /privacy page --- nextjs/src/app/(logged-out)/privacy/page.tsx | 419 ++++++++++++++++++ .../src/app/(logged-out)/privacy/privacy.css | 30 ++ nextjs/src/components/footer.tsx | 4 +- nextjs/src/components/navbar.tsx | 7 + 4 files changed, 458 insertions(+), 2 deletions(-) create mode 100644 nextjs/src/app/(logged-out)/privacy/page.tsx create mode 100644 nextjs/src/app/(logged-out)/privacy/privacy.css diff --git a/nextjs/src/app/(logged-out)/privacy/page.tsx b/nextjs/src/app/(logged-out)/privacy/page.tsx new file mode 100644 index 000000000..368545660 --- /dev/null +++ b/nextjs/src/app/(logged-out)/privacy/page.tsx @@ -0,0 +1,419 @@ +import { Metadata } from 'next' + +import './privacy.css' + +export default function PrivacyPage() { + return ( +
+

Privacy Policy

+

Effective date: 07/01/2025

+

1. Introduction

+

+ Welcome to Mapped! Mapped is developed and maintained by Common + Knowledge, a not-for-profit worker cooperative of technologists, + designers, researchers and facilitators. We work in direct collaboration + with grassroots organisers and communities around the world, using our + digital expertise to multiply their impact and capacity. +

+

+ Common Knowledge Cooperative Limited (“us”, “we”, or “our”) operates + https://mapped.tools (hereinafter referred to as “Service”). +

+

+ Our Privacy Policy governs your visit to https://mapped.tools and + explains how we collect, safeguard and disclose information that results + from your use of our Service. +

+

+ We use your data to provide and improve Service. By using Service, you + agree to the collection and use of information in accordance with this + policy. Unless otherwise defined in this Privacy Policy, the terms used + in this Privacy Policy have the same meanings as in our Terms and + Conditions. +

+

+ Our Terms of Service (“Terms”) govern all use of our Service and + together with the Privacy Policy constitutes your agreement with us + (“agreement”). +

+

2. Definitions

+

+ SERVICE means the + https://prototype.mapped.commonknowledge.coop/ website operated by + Common Knowledge Cooperative Limited. +

+

+ PERSONAL DATA means data about a living individual who + can be identified from those data (or from those and other information + either in our possession or likely to come into our possession). +

+

+ USAGE DATA is data collected automatically either + generated by the use of Service or from Service infrastructure itself + (for example, the duration of a page visit). +

+

+ COOKIES are small files stored on your device (computer + or mobile device). +

+

+ DATA CONTROLLER means a natural or legal person, public + authority, agency or other body which, alone or jointly with others, + determines the purposes and means of the processing of personal data. + For the purpose of this Privacy Policy, we are a Data Controller of your + data. +

+

+ DATA PROCESSORS (OR SERVICE PROVIDERS) means any + natural or legal person who processes the data on behalf of the Data + Controller. We may use the services of various Service Providers in + order to process your data more effectively. +

+

+ DATA SUBJECT is any living individual who is the + subject of Personal Data. +

+

+ PROCESSING in relation to personal data, means any + operation or set of operations which is performed on personal data or on + sets of personal data (whether or not by automated means, such as + collection, recording, organisation, structuring, storage, alteration, + retrieval, consultation, use, disclosure, dissemination, restriction, + erasure or destruction). +

+

+ THE USER is the individual using our Service. The User + corresponds to the Data Subject, who is the subject of Personal Data.{' '} +

+

3. Your data protection rights

+

Under data protection law, you have rights including:

+

+ Your right of access - You have the right to ask us for + copies of your personal information. +

+

+ Your right to rectification - You have the right to ask + us to rectify personal information you think is inaccurate. You also + have the right to ask us to complete information you think is + incomplete. +

+

+ Your right to erasure - You have the right to ask us to + erase your personal information in certain circumstances. +

+

+ Your right to restriction of processing - You have the + right to ask us to restrict the processing of your personal information + in certain circumstances. +

+

+ Your right to object to processing - You have the right + to object to the processing of your personal information in certain + circumstances. +

+

+ Your right to data portability - You have the right to + ask that we transfer the personal information you gave us to another + organisation, or to you, in certain circumstances. +

+

+ You are not required to pay any charge for exercising your rights. If + you make a request, we have one month to respond to you. +

+

+ Please contact us at hello@commonknowledge.coop if you wish to make a + request. +

+

4. Information Collection and Use

+

+ 4.1. We collect several different types of information + for various purposes to provide and improve our Service to you. +

+

5. Types of Data Collected

+

+ 5.1. Personal Data +

+

+ While using our Service, we may ask you to provide us with certain + personally identifiable information that can be used to contact or + identify you (“Personal Data”). Personally identifiable information may + include, but is not limited to: +

+
    +
  1. Email address
  2. +
  3. First name and last name
  4. +
  5. Address, Postal code, City
  6. +
  7. IP Address and Cookies
  8. +
+

+ We may use your Personal Data to contact you with newsletters, marketing + or promotional materials and other information that may be of interest + to you. You may opt out of receiving any, or all, of these + communications from us by emailing to hello@commonknowledge.coop +

+

+ 5.2. Usage Data +

+

+ We may also collect information that your browser sends whenever you + visit our Service (“Usage Data”). +

+

+ This Usage Data includes your computer's de-identified Internet Protocol + address (IP address), browser type, browser version, the pages of our + Service that you visit, the time and date of your visit, the time spent + on those pages, unique device identifiers and other diagnostic data. + Common Knowledge owns the intellectual property rights in the Usage + Data. +

+

+ 5.3. Tracking Cookies Data +

+

+ We use cookies and similar tracking technologies to track the activity + on our Service and we hold certain information. +

+

+ Cookies are files with a small amount of data which may include an + anonymous unique identifier. Cookies are sent to your browser from a + website and stored on your device. Other tracking technologies are also + used such as beacons, tags and scripts to collect and track information + and to improve and analyse our Service. +

+

+ You can instruct your browser to refuse all cookies or to indicate when + a cookie is being sent. However, if you do not accept cookies, you may + not be able to use some portions of our Service. +

+

Examples of Cookies we use:

+
    +
  1. Session Cookies: We use Session Cookies to operate our Service.
  2. +
  3. + Preference Cookies: We use Preference Cookies to remember your + preferences and various settings. +
  4. +
  5. + Security Cookies: We use Security Cookies for security purposes. +
  6. +
+

6. Use of Data

+

Common Knowledge uses the collected data for various purposes:

+
    +
  1. to provide and maintain our Service;
  2. +
  3. to notify you about changes to our Service;
  4. +
  5. + to allow you to participate in interactive features of our Service + when you choose to do so; +
  6. +
  7. to provide customer support;
  8. +
  9. + to gather analysis or valuable information so that we can improve our + Service; +
  10. +
  11. to monitor the usage of our Service;
  12. +
  13. to detect, prevent and address technical issues;
  14. +
  15. to fulfil any other purpose for which you provide it;
  16. +
  17. + to carry out our obligations and enforce our rights arising from any + contracts entered into between you and us, including for billing and + collection; +
  18. +
  19. + to provide you with notices about your account and/or subscription, + including expiration and renewal notices, email-instructions, etc.; +
  20. +
  21. + to provide you with news, special offers and general information about + other goods, services and events which we offer that are similar to + those that you have already purchased or enquired about unless you + have opted not to receive such information; +
  22. +
  23. + in any other way we may describe when you provide the information; +
  24. +
  25. for any other purpose with your consent.
  26. +
+

7. Retention of Data

+

+ 7.1. We will retain your Personal Data only for as long + as is necessary for the purposes set out in the Agreement and this + Privacy Policy but no more than twelve months after the termination of + your Account. We will retain and use your Personal Data to the extent + necessary to comply with our legal obligations (for example, if we are + required to retain your data to comply with applicable laws, resolve + disputes, and enforce our legal agreements and policies). +

+

8. Processing Location

+

+ 8.1. Your consent to this Privacy Policy followed by + your submission of such information represents your agreement to process + the data in the United Kingdom. +

+

9. Disclosure of Data

+

+ We may disclose personal information that we collect, or you provide: +

+
    +
  1. + Disclosure for Law Enforcement. Under certain + circumstances, we may be required to disclose your Personal Data if + required to do so by law or in response to valid requests by public + authorities. +
  2. +
  3. + Business Transaction. If we or our subsidiaries are + involved in a merger, acquisition or asset sale, your Personal Data + may be transferred. +
  4. +
  5. + Other cases. We may disclose your information also: +
      +
    1. to our subsidiaries and affiliates;
    2. +
    3. + to contractors, service providers, and other third parties we use + to support our business; +
    4. +
    5. to fulfil the purpose for which you provide it;
    6. +
    7. + for any other purpose disclosed by us when you provide the + information; +
    8. +
    9. with your consent in any other cases;
    10. +
    11. + if we believe disclosure is necessary or appropriate to protect + the rights, property, or safety of the Company, our customers, or + others. +
    12. +
    +
  6. +
+

10. Security of Data

+

+ The security of your data is important to us but remember that no method + of transmission over the Internet or method of electronic storage is + 100% secure. +

+

+ While we strive to use commercially acceptable means to protect your + Personal Data, we cannot guarantee its absolute security. +

+

11. Service Providers

+

+ We may employ third party companies and individuals to facilitate our + Service (“Service Providers”), provide Service on our behalf, perform + Service-related services or assist us in analysing how our Service is + used. +

+

+ These third parties have access to your Personal Data only to perform + these tasks on our behalf and are obligated not to disclose or use it + for any other purpose. +

+

12. Analytics

+

+ We may use third-party Service Providers to monitor and analyse the use + of the Service. +

+

+ 12.1. Sentry. Sentry is an open-source error tracking + solution provided by Functional Software Inc. More information is + available here: https://sentry.io/privacy/. To opt out of Sentry + tracking, see https://sentry.io/contact/gdpr/ +

+

+ 12.2. Posthog. Posthog is a product analytics service + we use to capture usage data, which we use to maintain and improve the + product. +

+

13. CI/CD tools

+

+ We may use third-party Service Providers to automate the development + process of our Service. +

+

+ 13.1. GitHub. GitHub is provided by GitHub, Inc. GitHub + is a development platform to host and review code, manage projects, and + build software. For more information on what data GitHub collects for + what purpose and how the protection of the data is ensured, please visit + GitHub Privacy Policy page: + https://help.github.com/en/articles/github-privacy-statement +

+

14. Links to Other Sites

+

+ Our Service may contain links to other sites that are not operated by + us. If you click a third party link, you will be directed to that third + party's site. We strongly advise you to review the Privacy Policy of + every site you visit. +

+

+ We have no control over and assume no responsibility for the content, + privacy policies or practices of any third party sites or services. +

+

15. Children's Privacy

+

+ Our Services are not intended for use by children age 13 and under for + users in the United States, Belgium, Denmark, Portugal, Sweden and the + United Kingdom, 14 and under in China, Spain, Bulgaria, Austria, Cyprus, + Italy and Lithuania, 15 and under in Slovenia, Check Republic, Greece + and France and 18 and under for India, Columbia, Indonesia, Egypt and 16 + and under all other countries (“Children”). +

+

+ We do not knowingly collect personally identifiable information from + Children. If you become aware that a Child has provided us with Personal + Data, please contact us. If we become aware that we have collected + Personal Data from Children without verification of parental consent, we + take steps to remove that information from our servers. +

+

16. Changes to This Privacy Policy

+

+ We may update our Privacy Policy from time to time. We will notify you + of any changes by posting the new Privacy Policy on this page. +

+

+ We will let you know via email and/or a prominent notice on our Service, + prior to the change becoming effective and update “effective date” at + the top of this Privacy Policy. +

+

+ You are advised to review this Privacy Policy periodically for any + changes. Changes to this Privacy Policy are effective when they are + posted on this page. +

+

16. How to complain

+

+ If you have any concerns about our use of your personal information, you + can make a complaint to us at data@commonknowledge.coop +

+

+ You can also complain to the ICO if you are unhappy with how we have + used your data. +

+

The ICO’s address:

+

+ Information Commissioner’s Office +
+ Wycliffe House +
+ Water Lane +
+ Wilmslow +
+ Cheshire +
+ SK9 5AF +

+

Helpline number: 0303 123 1113

+

ICO website: https://www.ico.org.uk

+

17. Contact us

+

+ If you have any questions relating to this Privacy Policy, please + contact: data@commonknowledge.coop +

+
+ ) +} + +export const metadata: Metadata = { + title: 'Privacy Policy', +} diff --git a/nextjs/src/app/(logged-out)/privacy/privacy.css b/nextjs/src/app/(logged-out)/privacy/privacy.css new file mode 100644 index 000000000..02e2f3470 --- /dev/null +++ b/nextjs/src/app/(logged-out)/privacy/privacy.css @@ -0,0 +1,30 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +nav { + @apply !relative; +} + +h1 { + @apply text-3xl font-bold; +} + +h2 { + @apply text-2xl font-bold; +} + +h1, +h2, +p, +ol { + @apply mb-2; +} + +strong { + @apply font-bold; +} + +ol { + @apply pl-8 list-decimal; +} diff --git a/nextjs/src/components/footer.tsx b/nextjs/src/components/footer.tsx index 4c0e381c7..0147031b6 100644 --- a/nextjs/src/components/footer.tsx +++ b/nextjs/src/components/footer.tsx @@ -21,8 +21,8 @@ export default function Footer() {
Privacy policy diff --git a/nextjs/src/components/navbar.tsx b/nextjs/src/components/navbar.tsx index 96c60fc09..35591cea5 100644 --- a/nextjs/src/components/navbar.tsx +++ b/nextjs/src/components/navbar.tsx @@ -195,6 +195,13 @@ export default function Navbar({ isLoggedIn }: NavbarProps) { About + + + Privacy + +