Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clustering member data by political geographies #28

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions hub/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from django.db.models import Count, F
import pandas as pd
from typing import List, Optional, TypedDict

class Analytics:
def __init__(self, qs) -> None:
self.qs = qs
janbaykara marked this conversation as resolved.
Show resolved Hide resolved

def get_dataframe(self, qs):
json_list = [{**d.postcode_data, **d.json} for d in self.qs]
enrichment_df = pd.DataFrame.from_records(json_list)
return enrichment_df

class RegionCount(TypedDict):
label: str
area_id: Optional[str]
count: int

def imported_data_count_by_region(self) -> List[RegionCount]:
return self.qs()\
.annotate(
label=F('postcode_data__european_electoral_region'),
area_id=F('postcode_data__codes__european_electoral_region')
)\
.values('label', 'area_id')\
.annotate(count=Count('label'))\
.order_by('-count')\


def imported_data_count_by_constituency(self) -> List[RegionCount]:
return self.qs()\
.annotate(
label=F('postcode_data__parliamentary_constituency'),
area_id=F('postcode_data__codes__parliamentary_constituency')
)\
.values('label', 'area_id')\
.annotate(count=Count('label'))\
.order_by('-count')\


def imported_data_count_by_constituency_2024(self) -> List[RegionCount]:
return self.qs()\
.annotate(
label=F('postcode_data__parliamentary_constituency_2025'),
area_id=F('postcode_data__codes__parliamentary_constituency_2025')
)\
.values('label', 'area_id')\
.annotate(count=Count('label'))\
.order_by('-count')\


def imported_data_count_by_council(self) -> List[RegionCount]:
return self.qs()\
.annotate(
label=F('postcode_data__admin_district'),
area_id=F('postcode_data__codes__admin_district')
)\
.values('label', 'area_id')\
.annotate(count=Count('label'))\
.order_by('-count')\


def imported_data_count_by_ward(self) -> List[RegionCount]:
return self.qs()\
.annotate(
label=F('postcode_data__admin_ward'),
area_id=F('postcode_data__codes__admin_ward')
)\
.values('label', 'area_id')\
.annotate(count=Count('label'))\
.order_by('-count')\

def imported_data_count(self) -> int:
count = self.qs().all().count()
if isinstance(count, int):
return count
return 0
1 change: 1 addition & 0 deletions hub/graphql/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class AirtableSourceInput(ExternalDataSourceInput):
class MapLayerInput:
name: str
source: str
visible: Optional[bool] = True


@strawberry_django.input(models.MapReport, partial=True)
Expand Down
68 changes: 50 additions & 18 deletions hub/graphql/types/geojson.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from enum import Enum
from typing import List, Optional, Union
from django.contrib.gis.geos import Point, Polygon, MultiPolygon

import strawberry
from strawberry.scalars import JSON

#

@strawberry.enum
class GeoJSONTypes(Enum):
Expand All @@ -13,45 +15,75 @@ class GeoJSONTypes(Enum):
Polygon = "Polygon"
MultiPolygon = "MultiPolygon"

#

@strawberry.type
class FeatureCollection:
type: GeoJSONTypes.FeatureCollection = GeoJSONTypes.FeatureCollection
features: List["Feature"]

#

@strawberry.interface
class Feature:
id: Optional[str]
type: GeoJSONTypes.Feature = GeoJSONTypes.Feature
properties: JSON
geometry: Union["PointGeometry", "PolygonGeometry", "MultiPolygonGeometry"]


@strawberry.type
class PointFeature(Feature):
geometry: "PointGeometry"


@strawberry.interface
class Geometry:
type: GeoJSONTypes
id: Optional[str]
properties: Optional[JSON]

#

@strawberry.type
class PointGeometry(Geometry):
class PointGeometry:
type: GeoJSONTypes.Point = GeoJSONTypes.Point
# lng, lat
coordinates: List[float]


@strawberry.type
class PolygonGeometry(Geometry):
class PointFeature(Feature):
geometry: PointGeometry

@classmethod
def from_geodjango(cls, point: Point, properties: dict = {}, id: str = None) -> "PointFeature":
return PointFeature(
id=str(id),
geometry=PointGeometry(coordinates=point),
properties=properties,
)

#

@strawberry.type
class PolygonGeometry:
type: GeoJSONTypes.Polygon = GeoJSONTypes.Polygon
coordinates: List[List[List[float]]]


@strawberry.type
class MultiPolygonGeometry(Geometry):
class PolygonFeature(Feature):
geometry: PolygonGeometry

@classmethod
def from_geodjango(cls, polygon: Polygon, properties: dict = {}, id: str = None) -> "PolygonFeature":
return PolygonFeature(
id=str(id),
geometry=PolygonGeometry(coordinates=polygon),
properties=properties,
)

#

@strawberry.type
class MultiPolygonGeometry:
type: GeoJSONTypes.MultiPolygon = GeoJSONTypes.MultiPolygon
coordinates: List[List[List[List[float]]]]

@strawberry.type
class MultiPolygonFeature(Feature):
geometry: MultiPolygonGeometry

@classmethod
def from_geodjango(cls, multipolygon: MultiPolygon, properties: dict = {}, id: str = None) -> "MultiPolygonFeature":
return MultiPolygonFeature(
id=str(id),
geometry=MultiPolygonGeometry(coordinates=multipolygon),
properties=properties,
)
91 changes: 79 additions & 12 deletions hub/graphql/types/model_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from strawberry import auto
from strawberry.types.info import Info
from strawberry_django.auth.utils import get_current_user
from strawberry.scalars import JSON

from hub import models
from hub.graphql.types.geojson import PointFeature, PointGeometry
from hub.graphql.utils import dict_key_field
from hub.graphql.types.geojson import PointFeature, MultiPolygonFeature
from hub.graphql.utils import dict_key_field, fn_field


@strawberry_django.filters.filter(
Expand Down Expand Up @@ -136,6 +137,63 @@ class ExternalDataSourceFilter:
geography_column_type: auto


@strawberry_django.type(models.Area)
class Area:
mapit_id: auto
gss: auto
name: auto
area_type: auto
geometry: auto
overlaps: auto
# So that we can pass in properties to the geojson Feature objects
extra_geojson_properties: strawberry.Private[object]

@strawberry_django.field
def polygon(self, info: Info, with_parent_data: bool = False) -> Optional[MultiPolygonFeature]:
props = {
"name": self.name,
"gss": self.gss
}
if with_parent_data and hasattr(self, "extra_geojson_properties"):
props["extra_geojson_properties"] = self.extra_geojson_properties

return MultiPolygonFeature.from_geodjango(
multipolygon=self.polygon,
id=self.gss,
properties=props
)

@strawberry_django.field
def point(self, info: Info, with_parent_data: bool = False) -> Optional[PointFeature]:
props = {
"name": self.name,
"gss": self.gss
}
if with_parent_data and hasattr(self, "extra_geojson_properties"):
props["extra_geojson_properties"] = self.extra_geojson_properties

return PointFeature.from_geodjango(
point=self.point,
id=self.gss,
properties=props
)


@strawberry.type
class GroupedDataCount:
label: Optional[str] = dict_key_field()
area_id: Optional[str] = dict_key_field()
count: int = dict_key_field()

@strawberry_django.field
def gss_area(self, info: Info) -> Optional[Area]:
conatus marked this conversation as resolved.
Show resolved Hide resolved
if self.get('area_id', None):
area = models.Area.objects.get(gss=self['area_id'])
area.extra_geojson_properties = self
return area
return None


@strawberry_django.type(models.ExternalDataSource, filters=ExternalDataSourceFilter)
class ExternalDataSource:
id: auto
Expand Down Expand Up @@ -201,25 +259,26 @@ def webhook_healthcheck(self: models.ExternalDataSource, info) -> bool:
return self.webhook_healthcheck()

@strawberry_django.field
def imported_data_count(self: models.ExternalDataSource, info: Info) -> int:
return self.imported_data_count()

@strawberry_django.field
def geojson_point_features(
def imported_data_geojson_points(
self: models.ExternalDataSource, info: Info
) -> List[PointFeature]:
data = self.get_import_data()
return [
PointFeature(
id=str(generic_datum.data),
geometry=PointGeometry(
coordinates=[generic_datum.point.x, generic_datum.point.y]
),
PointFeature.from_geodjango(
point=generic_datum.point,
id=generic_datum.data,
properties=generic_datum.json,
)
for generic_datum in data
if generic_datum.point is not None
]

imported_data_count: int = fn_field()
imported_data_count_by_region: List[GroupedDataCount] = fn_field()
imported_data_count_by_constituency: List[GroupedDataCount] = fn_field()
imported_data_count_by_constituency_2024: List[GroupedDataCount] = fn_field()
imported_data_count_by_council: List[GroupedDataCount] = fn_field()
imported_data_count_by_ward: List[GroupedDataCount] = fn_field()

@strawberry_django.field
def is_importing(self: models.ExternalDataSource, info: Info) -> bool:
Expand Down Expand Up @@ -263,6 +322,7 @@ def get_queryset(cls, queryset, info, **kwargs):
@strawberry.type
class MapLayer:
name: str = dict_key_field()
visible: Optional[bool] = dict_key_field()

@strawberry_django.field
def source(self, info: Info) -> ExternalDataSource:
Expand All @@ -273,3 +333,10 @@ def source(self, info: Info) -> ExternalDataSource:
@strawberry_django.type(models.MapReport)
class MapReport(Report):
layers: Optional[List[MapLayer]]

imported_data_count: int = fn_field()
imported_data_count_by_region: List[GroupedDataCount] = fn_field()
imported_data_count_by_constituency: List[GroupedDataCount] = fn_field()
imported_data_count_by_constituency_2024: List[GroupedDataCount] = fn_field()
imported_data_count_by_council: List[GroupedDataCount] = fn_field()
imported_data_count_by_ward: List[GroupedDataCount] = fn_field()
20 changes: 15 additions & 5 deletions hub/graphql/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import strawberry
import strawberry_django
from strawberry.types.info import Info

def attr_resolver(root, info: Info):
return getattr(root, info.python_name, None)

def dict_key(root, info: Info) -> str:
return root.get(info.python_name, None)
def attr_field(**kwargs):
return strawberry_django.field(resolver=attr_resolver, **kwargs)

def fn_resolver(root, info: Info):
return getattr(root, info.python_name, lambda: None)()

def fn_field(**kwargs):
return strawberry_django.field(resolver=fn_resolver, **kwargs)

def dict_resolver(root, info: Info):
return root.get(info.python_name, None)

def dict_key_field():
return strawberry.field(resolver=dict_key)
def dict_key_field(**kwargs):
return strawberry_django.field(resolver=dict_resolver, **kwargs)
Loading
Loading