Skip to content

Commit 2c959ce

Browse files
committed
Refactor before implementing appending territory of other level than country
1 parent 716933c commit 2c959ce

File tree

5 files changed

+78
-36
lines changed

5 files changed

+78
-36
lines changed

src/d3/geo-map.d3.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,21 @@ import { BaseD3, RenderOptions as BaseRenderOptions } from './base.d3';
33
import * as topojson from 'topojson';
44
import { GeometryCollection, Topology } from 'topojson-specification';
55
import { Observable } from 'rxjs';
6-
import { GeoDatum } from '../datasets/queries/geo.query';
6+
import { GeoDatum, TerritoryLevel } from '../datasets/queries/geo.query';
77
import * as GeoJSON from 'geojson';
88
import { linearScale } from '../utils/misc';
99

1010
type CountryGeometry = GeoJSON.Polygon | GeoJSON.MultiPolygon;
11-
type CountrySelectionDatum =
12-
GeoJSON.Feature<GeoJSON.Geometry, CountryGeometry>
13-
| GeoJSON.FeatureCollection<GeoJSON.Geometry, CountryGeometry>;
1411
type WorldTopology = Topology<{ land: GeometryCollection, countries: GeometryCollection<CountryGeometry> }>;
1512

1613
export interface RenderOptions extends BaseRenderOptions {
1714
topoJsonUrl: string;
1815
data$: Observable<GeoDatum[]>;
1916
}
2017

18+
type AppendTerritoryPathFunction<L extends TerritoryLevel> =
19+
(datum: GeoDatum<L>, maxValue: number) => d3.Selection<SVGPathElement, any, null, undefined> | null;
20+
2121
export class GeoMapD3 extends BaseD3<RenderOptions> {
2222
static minOpacity = .2;
2323
static latitudeBounds = [-84, 84];
@@ -33,7 +33,7 @@ export class GeoMapD3 extends BaseD3<RenderOptions> {
3333
private landPath: d3.Selection<SVGPathElement, GeoJSON.FeatureCollection<GeoJSON.Geometry, {}>, null, undefined>;
3434
private boundaryPath: d3.Selection<SVGPathElement, GeoJSON.MultiLineString, null, undefined>;
3535
private dataG: d3.Selection<SVGGElement, unknown, null, undefined>;
36-
private dataPaths: d3.Selection<SVGPathElement, CountrySelectionDatum, null, undefined>[];
36+
private territoryPaths: d3.Selection<SVGPathElement, any, null, undefined>[];
3737

3838
private static accessValue(datum: GeoDatum) {
3939
return datum.values.activeUsers;
@@ -181,7 +181,7 @@ export class GeoMapD3 extends BaseD3<RenderOptions> {
181181

182182
this.lastTransform = transform;
183183

184-
[this.landPath, this.boundaryPath, ...this.dataPaths]
184+
[this.landPath, this.boundaryPath, ...this.territoryPaths]
185185
.forEach(dataPath => dataPath.attr('d', this.geoPath));
186186
}
187187

@@ -190,29 +190,63 @@ export class GeoMapD3 extends BaseD3<RenderOptions> {
190190

191191
private renderData() {
192192
this.dataG = this.svg.insert('g', '.geo_map-boundary');
193-
this.dataPaths = [];
193+
this.territoryPaths = [];
194194
}
195195

196-
private updateData(data: GeoDatum[]) {
196+
private updateData<L extends TerritoryLevel>(data: GeoDatum<L>[]) {
197197
this.dataG.html('');
198198

199-
const { colorPrimary, minOpacity, accessValue } = GeoMapD3;
199+
const { accessValue } = GeoMapD3;
200+
const maxValue = data.reduce((acc, datum) => Math.max(acc, accessValue(datum)), 0);
201+
this.territoryPaths = data
202+
.map(datum => this.appendTerritoryPath(datum, maxValue))
203+
.filter((dataPath): dataPath is d3.Selection<SVGPathElement, any, null, undefined> => dataPath !== null);
204+
}
205+
206+
private appendTerritoryPath<L extends TerritoryLevel>(datum: GeoDatum<L>, maxValue: number) {
207+
const appendFunction = {
208+
[TerritoryLevel.CONTINENT]: this.appendContinentPath,
209+
[TerritoryLevel.SUBCONTINENT]: this.appendSubcontinentPath,
210+
[TerritoryLevel.COUNTRY]: this.appendCountryPath,
211+
[TerritoryLevel.CITY]: this.appendCityPath,
212+
}[datum.territory.level] as AppendTerritoryPathFunction<L>;
213+
return appendFunction.call(this, datum, maxValue);
214+
}
215+
216+
private appendContinentPath: AppendTerritoryPathFunction<TerritoryLevel.CONTINENT> = (datum, maxValue) => {
217+
return null;
218+
};
219+
220+
private appendSubcontinentPath: AppendTerritoryPathFunction<TerritoryLevel.SUBCONTINENT> = (datum, maxValue) => {
221+
return null;
222+
};
200223

224+
private appendCountryPath: AppendTerritoryPathFunction<TerritoryLevel.COUNTRY> = (datum, maxValue) => {
225+
const { accessValue } = GeoMapD3;
201226
const { countries } = this.worldTopology.objects;
202-
const maxValue = data.reduce((acc, datum) => Math.max(acc, accessValue(datum)), 0);
227+
const countryGeometryObject = countries.geometries.find(geometry => geometry.id === datum.territory.id);
228+
if (!countryGeometryObject) {
229+
return null;
230+
}
231+
const valueRatio = accessValue(datum) / maxValue;
232+
return this.dataG
233+
.append('path')
234+
.datum(topojson.feature(this.worldTopology, countryGeometryObject))
235+
.attr('d', this.geoPath)
236+
.call(this.styleTerritory(valueRatio));
237+
};
238+
239+
private appendCityPath: AppendTerritoryPathFunction<TerritoryLevel.CITY> = (datum, maxValue) => {
240+
return null;
241+
};
242+
243+
private styleTerritory<T extends SVGGraphicsElement>(valueRatio: number) {
244+
const { colorPrimary, minOpacity } = GeoMapD3;
203245

204-
this.dataPaths = data.map(datum => {
205-
const countryGeometryObject = countries.geometries.find(geometry => geometry.id === datum.id);
206-
if (!countryGeometryObject) {
207-
return null;
208-
}
209-
const valueRatio = accessValue(datum) / maxValue;
210-
return this.dataG
211-
.append('path')
212-
.datum(topojson.feature(this.worldTopology, countryGeometryObject))
213-
.attr('d', this.geoPath)
246+
return (selection: d3.Selection<SVGPathElement, any, null, undefined>) => {
247+
selection
214248
.attr('fill', colorPrimary)
215249
.attr('opacity', minOpacity + valueRatio * (1 - minOpacity));
216-
}).filter((dataPath): dataPath is d3.Selection<SVGPathElement, CountrySelectionDatum, null, undefined> => dataPath !== null);
250+
};
217251
}
218252
}

src/d3/line-chart.d3.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ export class LineChartD3 extends XYChartD3<LegendItemStyle> {
105105
...LineChartD3.defaultLegendItemStyle,
106106
...style ?? {},
107107
};
108-
return (stylePath: d3.Selection<T, unknown, null, undefined>) => {
109-
stylePath
108+
return (selection: d3.Selection<T, unknown, null, undefined>) => {
109+
selection
110110
.attr('stroke', color)
111111
.attr('stroke-width', width)
112112
.attr('opacity', opacity)

src/datasets/queries/geo.query.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ export enum TerritoryLevel {
99
CITY,
1010
}
1111

12-
export interface Territory {
13-
level: TerritoryLevel;
12+
export interface Territory<L extends TerritoryLevel> {
13+
level: L;
1414
id: string;
1515
}
1616

17-
export interface GeoQueryOptions {
17+
export interface GeoQueryOptions<L extends TerritoryLevel> {
1818
/** The date range to filter geo data with. */
1919
range: [Date, Date];
2020
/** The territory to filter geo data with. */
21-
territory?: Territory;
21+
territory?: Territory<TerritoryLevel>;
2222
/** The territory level of each geo datum. */
23-
unit: TerritoryLevel;
23+
unit: L;
2424
}
2525

26-
export interface GeoDatum {
27-
id: string;
26+
export interface GeoDatum<L extends TerritoryLevel = TerritoryLevel> {
27+
territory: Territory<L>;
2828
values: MeasureValues;
2929
}
3030

31-
export type GeoQuery = (options: GeoQueryOptions) => GeoDatum[];
31+
export type GeoQuery<L extends TerritoryLevel = TerritoryLevel> = (options: GeoQueryOptions<L>) => GeoDatum<L>[];
3232

3333
export interface City {
3434
countryId: string;
@@ -47,11 +47,11 @@ export interface City {
4747
* @param measureNames The measures to query.
4848
* @param cities The mapping from city id to city object.
4949
*/
50-
export function createGeoQuery<S>(
50+
export function createGeoQuery<L extends TerritoryLevel = TerritoryLevel>(
5151
dataCube: DataCube,
5252
measureNames: string[],
5353
cities: Record<string, City>,
54-
): GeoQuery {
54+
): GeoQuery<L> {
5555
return queryOptions => {
5656
const [startDate, endDate] = queryOptions.range;
5757

@@ -85,11 +85,17 @@ export function createGeoQuery<S>(
8585
}
8686
}
8787

88-
return Object.entries(rowGroups).map(([id, values]) => ({ id, values }));
88+
return Object.entries(rowGroups).map(([id, values]) => ({
89+
territory: {
90+
level: queryOptions.unit,
91+
id,
92+
},
93+
values,
94+
}));
8995
};
9096
}
9197

92-
function getCityIds(cities: Record<string, City>, territory: Territory) {
98+
function getCityIds(cities: Record<string, City>, territory: Territory<TerritoryLevel>) {
9399
const idAccessor = getIdAccessor(territory.level);
94100
return Object.entries(cities)
95101
.filter(cityEntry => idAccessor(cityEntry) === territory.id)

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"strictFunctionTypes": true,
2020
"removeComments": true,
2121
"preserveConstEnums": true,
22+
"strictBindCallApply": true,
2223
"resolveJsonModule": true
2324
},
2425
"angularCompilerOptions": {

tslint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@
8282
],
8383
"semicolon": {
8484
"options": [
85-
"always"
85+
"always",
86+
"strict-bound-class-methods"
8687
]
8788
},
8889
"space-before-function-paren": {

0 commit comments

Comments
 (0)