geo-finance 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: 20
16
+ registry-url: 'https://registry.npmjs.org'
17
+ - run: npm publish
18
+ env:
19
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN}}
20
+
@@ -0,0 +1,12 @@
1
+ import { autoUpdate } from "../src/automation/updater.js";
2
+
3
+ async function main() {
4
+ const enrichedRegions = await autoUpdate(
5
+ "./examples/russia_regions.geojson",
6
+ "./examples/regional_budget.csv"
7
+ );
8
+
9
+ console.log("Regions enriched:", enrichedRegions);
10
+ }
11
+
12
+ main();
@@ -0,0 +1,4 @@
1
+ regionId,year,income,expense,investment
2
+ RU-MOW,2023,4500,3800,900
3
+ RU-SPE,2023,1200,1100,200
4
+ RU-MO,2023,950,980,120
@@ -0,0 +1,421 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "properties": { "id": "RU-AD", "name": "Адыгея" },
7
+ "geometry": {
8
+ "type": "Polygon",
9
+ "coordinates": [
10
+ [
11
+ [39, 44],
12
+ [39, 45],
13
+ [40, 45],
14
+ [40, 44],
15
+ [39, 44]
16
+ ]
17
+ ]
18
+ }
19
+ },
20
+ {
21
+ "type": "Feature",
22
+ "properties": { "id": "RU-AL", "name": "Алтай" },
23
+ "geometry": {
24
+ "type": "Polygon",
25
+ "coordinates": [
26
+ [
27
+ [83, 51],
28
+ [83, 52],
29
+ [85, 52],
30
+ [85, 51],
31
+ [83, 51]
32
+ ]
33
+ ]
34
+ }
35
+ },
36
+ {
37
+ "type": "Feature",
38
+ "properties": { "id": "RU-ALT", "name": "Алтайский край" },
39
+ "geometry": {
40
+ "type": "Polygon",
41
+ "coordinates": [
42
+ [
43
+ [82, 51],
44
+ [82, 53],
45
+ [85, 53],
46
+ [85, 51],
47
+ [82, 51]
48
+ ]
49
+ ]
50
+ }
51
+ },
52
+ {
53
+ "type": "Feature",
54
+ "properties": { "id": "RU-AMU", "name": "Амурская область" },
55
+ "geometry": {
56
+ "type": "Polygon",
57
+ "coordinates": [
58
+ [
59
+ [124, 50],
60
+ [124, 54],
61
+ [130, 54],
62
+ [130, 50],
63
+ [124, 50]
64
+ ]
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ "type": "Feature",
70
+ "properties": { "id": "RU-ARK", "name": "Архангельская область" },
71
+ "geometry": {
72
+ "type": "Polygon",
73
+ "coordinates": [
74
+ [
75
+ [39, 62],
76
+ [39, 66],
77
+ [50, 66],
78
+ [50, 62],
79
+ [39, 62]
80
+ ]
81
+ ]
82
+ }
83
+ },
84
+ {
85
+ "type": "Feature",
86
+ "properties": { "id": "RU-AST", "name": "Астраханская область" },
87
+ "geometry": {
88
+ "type": "Polygon",
89
+ "coordinates": [
90
+ [
91
+ [46, 46],
92
+ [46, 49],
93
+ [50, 49],
94
+ [50, 46],
95
+ [46, 46]
96
+ ]
97
+ ]
98
+ }
99
+ },
100
+ {
101
+ "type": "Feature",
102
+ "properties": { "id": "RU-BEL", "name": "Белгородская область" },
103
+ "geometry": {
104
+ "type": "Polygon",
105
+ "coordinates": [
106
+ [
107
+ [35, 49],
108
+ [35, 51],
109
+ [37, 51],
110
+ [37, 49],
111
+ [35, 49]
112
+ ]
113
+ ]
114
+ }
115
+ },
116
+ {
117
+ "type": "Feature",
118
+ "properties": { "id": "RU-BRY", "name": "Брянская область" },
119
+ "geometry": {
120
+ "type": "Polygon",
121
+ "coordinates": [
122
+ [
123
+ [32, 52],
124
+ [32, 53],
125
+ [35, 53],
126
+ [35, 52],
127
+ [32, 52]
128
+ ]
129
+ ]
130
+ }
131
+ },
132
+ {
133
+ "type": "Feature",
134
+ "properties": { "id": "RU-BU", "name": "Бурятия" },
135
+ "geometry": {
136
+ "type": "Polygon",
137
+ "coordinates": [
138
+ [
139
+ [101, 50],
140
+ [101, 53],
141
+ [108, 53],
142
+ [108, 50],
143
+ [101, 50]
144
+ ]
145
+ ]
146
+ }
147
+ },
148
+ {
149
+ "type": "Feature",
150
+ "properties": { "id": "RU-CE", "name": "Чечня" },
151
+ "geometry": {
152
+ "type": "Polygon",
153
+ "coordinates": [
154
+ [
155
+ [44, 42],
156
+ [44, 44],
157
+ [46, 44],
158
+ [46, 42],
159
+ [44, 42]
160
+ ]
161
+ ]
162
+ }
163
+ },
164
+ {
165
+ "type": "Feature",
166
+ "properties": { "id": "RU-CHE", "name": "Челябинская область" },
167
+ "geometry": {
168
+ "type": "Polygon",
169
+ "coordinates": [
170
+ [
171
+ [58, 52],
172
+ [58, 56],
173
+ [62, 56],
174
+ [62, 52],
175
+ [58, 52]
176
+ ]
177
+ ]
178
+ }
179
+ },
180
+ {
181
+ "type": "Feature",
182
+ "properties": { "id": "RU-CHU", "name": "Чукотский АО" },
183
+ "geometry": {
184
+ "type": "Polygon",
185
+ "coordinates": [
186
+ [
187
+ [170, 65],
188
+ [170, 70],
189
+ [180, 70],
190
+ [180, 65],
191
+ [170, 65]
192
+ ]
193
+ ]
194
+ }
195
+ },
196
+ {
197
+ "type": "Feature",
198
+ "properties": { "id": "RU-CK", "name": "Чувашия" },
199
+ "geometry": {
200
+ "type": "Polygon",
201
+ "coordinates": [
202
+ [
203
+ [46, 54],
204
+ [46, 56],
205
+ [48, 56],
206
+ [48, 54],
207
+ [46, 54]
208
+ ]
209
+ ]
210
+ }
211
+ },
212
+ {
213
+ "type": "Feature",
214
+ "properties": { "id": "RU-DA", "name": "Дагестан" },
215
+ "geometry": {
216
+ "type": "Polygon",
217
+ "coordinates": [
218
+ [
219
+ [46, 41],
220
+ [46, 44],
221
+ [48, 44],
222
+ [48, 41],
223
+ [46, 41]
224
+ ]
225
+ ]
226
+ }
227
+ },
228
+ {
229
+ "type": "Feature",
230
+ "properties": { "id": "RU-IN", "name": "Ингушетия" },
231
+ "geometry": {
232
+ "type": "Polygon",
233
+ "coordinates": [
234
+ [
235
+ [44, 42],
236
+ [44, 43],
237
+ [46, 43],
238
+ [46, 42],
239
+ [44, 42]
240
+ ]
241
+ ]
242
+ }
243
+ },
244
+ {
245
+ "type": "Feature",
246
+ "properties": { "id": "RU-IRK", "name": "Иркутская область" },
247
+ "geometry": {
248
+ "type": "Polygon",
249
+ "coordinates": [
250
+ [
251
+ [99, 51],
252
+ [99, 56],
253
+ [110, 56],
254
+ [110, 51],
255
+ [99, 51]
256
+ ]
257
+ ]
258
+ }
259
+ },
260
+ {
261
+ "type": "Feature",
262
+ "properties": { "id": "RU-IVA", "name": "Ивановская область" },
263
+ "geometry": {
264
+ "type": "Polygon",
265
+ "coordinates": [
266
+ [
267
+ [40, 56],
268
+ [40, 57],
269
+ [42, 57],
270
+ [42, 56],
271
+ [40, 56]
272
+ ]
273
+ ]
274
+ }
275
+ },
276
+ {
277
+ "type": "Feature",
278
+ "properties": { "id": "RU-YAN", "name": "ЯНАО" },
279
+ "geometry": {
280
+ "type": "Polygon",
281
+ "coordinates": [
282
+ [
283
+ [70, 65],
284
+ [70, 72],
285
+ [85, 72],
286
+ [85, 65],
287
+ [70, 65]
288
+ ]
289
+ ]
290
+ }
291
+ },
292
+ {
293
+ "type": "Feature",
294
+ "properties": { "id": "RU-KAM", "name": "Камчатский край" },
295
+ "geometry": {
296
+ "type": "Polygon",
297
+ "coordinates": [
298
+ [
299
+ [156, 51],
300
+ [156, 60],
301
+ [166, 60],
302
+ [166, 51],
303
+ [156, 51]
304
+ ]
305
+ ]
306
+ }
307
+ },
308
+ {
309
+ "type": "Feature",
310
+ "properties": { "id": "RU-KB", "name": "Кабардино-Балкария" },
311
+ "geometry": {
312
+ "type": "Polygon",
313
+ "coordinates": [
314
+ [
315
+ [41, 42],
316
+ [41, 44],
317
+ [44, 44],
318
+ [44, 42],
319
+ [41, 42]
320
+ ]
321
+ ]
322
+ }
323
+ },
324
+ {
325
+ "type": "Feature",
326
+ "properties": { "id": "RU-KL", "name": "Калмыкия" },
327
+ "geometry": {
328
+ "type": "Polygon",
329
+ "coordinates": [
330
+ [
331
+ [44, 46],
332
+ [44, 48],
333
+ [47, 48],
334
+ [47, 46],
335
+ [44, 46]
336
+ ]
337
+ ]
338
+ }
339
+ },
340
+ {
341
+ "type": "Feature",
342
+ "properties": { "id": "RU-KLU", "name": "Калужская область" },
343
+ "geometry": {
344
+ "type": "Polygon",
345
+ "coordinates": [
346
+ [
347
+ [35, 54],
348
+ [35, 56],
349
+ [37, 56],
350
+ [37, 54],
351
+ [35, 54]
352
+ ]
353
+ ]
354
+ }
355
+ },
356
+ {
357
+ "type": "Feature",
358
+ "properties": { "id": "RU-KAMO", "name": "Камчатка" },
359
+ "geometry": {
360
+ "type": "Polygon",
361
+ "coordinates": [
362
+ [
363
+ [156, 51],
364
+ [156, 60],
365
+ [166, 60],
366
+ [166, 51],
367
+ [156, 51]
368
+ ]
369
+ ]
370
+ }
371
+ },
372
+ {
373
+ "type": "Feature",
374
+ "properties": { "id": "RU-MOW", "name": "Москва" },
375
+ "geometry": {
376
+ "type": "Polygon",
377
+ "coordinates": [
378
+ [
379
+ [37.3, 55.5],
380
+ [37.3, 55.9],
381
+ [37.9, 55.9],
382
+ [37.9, 55.5],
383
+ [37.3, 55.5]
384
+ ]
385
+ ]
386
+ }
387
+ },
388
+ {
389
+ "type": "Feature",
390
+ "properties": { "id": "RU-MO", "name": "Московская область" },
391
+ "geometry": {
392
+ "type": "Polygon",
393
+ "coordinates": [
394
+ [
395
+ [35.5, 54.5],
396
+ [35.5, 56.5],
397
+ [39, 56.5],
398
+ [39, 54.5],
399
+ [35.5, 54.5]
400
+ ]
401
+ ]
402
+ }
403
+ },
404
+ {
405
+ "type": "Feature",
406
+ "properties": { "id": "RU-SPE", "name": "Санкт-Петербург" },
407
+ "geometry": {
408
+ "type": "Polygon",
409
+ "coordinates": [
410
+ [
411
+ [29.6, 59.7],
412
+ [29.6, 60.2],
413
+ [30.6, 60.2],
414
+ [30.6, 59.7],
415
+ [29.6, 59.7]
416
+ ]
417
+ ]
418
+ }
419
+ }
420
+ ]
421
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "geo-finance",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "demo": "node --loader ts-node/esm ./examples/demo.ts"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "license": "ISC",
12
+ "devDependencies": {
13
+ "@types/d3-geo": "^3.1.0",
14
+ "@types/geojson": "^7946.0.16",
15
+ "@types/leaflet": "^1.9.21",
16
+ "@types/node": "^25.0.3",
17
+ "@types/pdfkit": "^0.17.4",
18
+ "ts-node": "^10.9.2",
19
+ "typescript": "^5.9.3"
20
+ },
21
+ "dependencies": {
22
+ "@turf/helpers": "^7.3.1",
23
+ "canvas": "^3.2.0",
24
+ "csv-parse": "^6.1.0",
25
+ "d3-geo": "^3.1.1",
26
+ "geojson": "^0.5.0",
27
+ "leaflet": "^1.9.4",
28
+ "pdfkit": "^0.17.2"
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ import { FinancialFlow } from '../core/types.js';
2
+
3
+ export function compareRegions(r1: FinancialFlow, r2: FinancialFlow) {
4
+ return {
5
+ incomeDiff: (r1.income || 0) - (r2.income || 0),
6
+ expenseDiff: (r1.expense || 0) - (r2.expense || 0),
7
+ indexDiff: (r1.financialIndex || 0) - (r2.financialIndex || 0)
8
+ };
9
+ }
@@ -0,0 +1,21 @@
1
+ import { Region, BudgetData, FinancialFlow } from "../core/types.js";
2
+ import { calculateFinancialIndex } from "../core/utils.js";
3
+
4
+ export function enrichRegionsWithFinance(
5
+ regions: Region[],
6
+ budgets: BudgetData[]
7
+ ): FinancialFlow[] {
8
+ return regions.map((r) => {
9
+ const data = budgets.find((b) => b.regionId === r.id);
10
+ const flow: FinancialFlow = { ...r };
11
+
12
+ if (data) {
13
+ flow.income = data.income;
14
+ flow.expense = data.expense;
15
+ flow.investment = data.investment;
16
+ flow.financialIndex = calculateFinancialIndex(r);
17
+ }
18
+
19
+ return flow;
20
+ });
21
+ }
@@ -0,0 +1,10 @@
1
+ import { enrichRegionsWithFinance } from "../analytics/financialIndex.js";
2
+ import { loadFinanceData } from "../io/financeLoader.js";
3
+ import { loadRegions } from "../io/geojsonLoader.js";
4
+
5
+ export async function autoUpdate(regionPath: string, financialPath: string) {
6
+ const regions = await loadRegions(regionPath);
7
+ const budgets = await loadFinanceData(financialPath);
8
+
9
+ return enrichRegionsWithFinance(regions, budgets);
10
+ }
@@ -0,0 +1,21 @@
1
+ export interface Region {
2
+ id: string;
3
+ name: string;
4
+ geometry: GeoJSON.Polygon | GeoJSON.MultiPolygon;
5
+ properties?: Record<string, unknown>;
6
+ }
7
+
8
+ export interface BudgetData {
9
+ regionId: string;
10
+ year: number;
11
+ income: number;
12
+ expense: number;
13
+ investment: number;
14
+ }
15
+
16
+ export interface FinancialFlow extends Region {
17
+ income?: number;
18
+ expense?: number;
19
+ investment?: number;
20
+ financialIndex?: number;
21
+ }
@@ -0,0 +1,21 @@
1
+ import { FinancialFlow } from "./types.js";
2
+
3
+ export function calculateFinancialIndex(region: FinancialFlow): number {
4
+ if (!region.income || !region.expense) return 0;
5
+ return (
6
+ region.income -
7
+ region.expense / (region.expense || 1) +
8
+ (region.investment || 0) / 1_000_000
9
+ );
10
+ }
11
+
12
+ export function detectAnomalies(
13
+ regions: FinancialFlow[],
14
+ threshold: number = 1.5
15
+ ) {
16
+ return regions.filter(
17
+ (r) =>
18
+ (r.financialIndex || 0) > threshold ||
19
+ (r.financialIndex || 0) < -threshold
20
+ );
21
+ }
@@ -0,0 +1,11 @@
1
+ import fs from 'fs';
2
+ import { parse } from 'csv-parse/sync';
3
+ import { BudgetData } from '../core/types.js';
4
+
5
+ export async function loadFinanceData(filePath: string): Promise<BudgetData[]> {
6
+ const content = fs.readFileSync(filePath, 'utf-8');
7
+ return parse(content, {
8
+ columns: true,
9
+ skip_empty_lines: true
10
+ });
11
+ }
@@ -0,0 +1,14 @@
1
+ import fs from 'fs';
2
+ import { Region } from "../core/types.js";
3
+ import { Feature } from "geojson";
4
+
5
+ export async function loadRegions(filepath: string): Promise<Region[]> {
6
+ const data = fs.readFileSync(filepath, "utf-8");
7
+ const geojson = JSON.parse(data);
8
+ return geojson.features.map((f: Feature) => ({
9
+ id: f.properties?.id,
10
+ name: f.properties?.name,
11
+ geometry: f.geometry,
12
+ properties: f.properties,
13
+ }));
14
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "resolveJsonModule": true,
9
+ "outDir": "dist",
10
+ "baseUrl": ".",
11
+ "paths": {
12
+ "*": ["src/*"]
13
+ },
14
+ "types": ["node"]
15
+ },
16
+ "include": ["src/**/*.ts", "examples/**/*.ts"]
17
+ }