@kiva/kv-components 3.90.4 → 3.91.0
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.
- package/CHANGELOG.md +22 -0
- package/data/countries-borders.json +1 -0
- package/package.json +2 -2
- package/tests/fixtures/mockLenderCountries.js +674 -0
- package/tests/unit/specs/utils/{mapAnimationUtils.spec.js → mapUtils.spec.js} +34 -2
- package/utils/{mapAnimation.js → mapUtils.js} +90 -0
- package/vue/KvIntroductionLoanCard.vue +1 -4
- package/vue/KvMap.vue +150 -7
- package/vue/stories/KvMap.stories.js +21 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import kvTokensPrimitives from '@kiva/kv-tokens/primitives.json';
|
|
1
2
|
import {
|
|
2
3
|
getCoordinatesBetween,
|
|
3
|
-
|
|
4
|
+
getLoansIntervals,
|
|
5
|
+
getCountryColor,
|
|
6
|
+
} from '../../../../utils/mapUtils';
|
|
7
|
+
import mockLenderCountries from '../../../fixtures/mockLenderCountries';
|
|
4
8
|
|
|
5
|
-
describe('
|
|
9
|
+
describe('mapUtils', () => {
|
|
6
10
|
describe('getCoordinatesBetween', () => {
|
|
7
11
|
it('should return empty array if inputs are invalid', () => {
|
|
8
12
|
expect(getCoordinatesBetween([100, 88], undefined, 10)).toStrictEqual([]);
|
|
@@ -10,6 +14,7 @@ describe('mapAnimation', () => {
|
|
|
10
14
|
expect(getCoordinatesBetween(undefined, undefined, 0)).toStrictEqual([]);
|
|
11
15
|
expect(getCoordinatesBetween(undefined, [-89, -89], 0)).toStrictEqual([]);
|
|
12
16
|
});
|
|
17
|
+
|
|
13
18
|
it('should return an array of steps', () => {
|
|
14
19
|
expect(getCoordinatesBetween([0, 0], [100, 100], 10)).toStrictEqual(
|
|
15
20
|
[[0, 0], [10, 10], [20, 20], [30, 30], [40, 40], [50, 50], [60, 60], [70, 70], [80, 80], [100, 100]],
|
|
@@ -21,20 +26,47 @@ describe('mapAnimation', () => {
|
|
|
21
26
|
[[0, 0], [100, 101]],
|
|
22
27
|
);
|
|
23
28
|
});
|
|
29
|
+
|
|
24
30
|
it('should handle negative numbers', () => {
|
|
25
31
|
expect(getCoordinatesBetween([-10, -30], [-90, -45.123], 3)).toStrictEqual(
|
|
26
32
|
[[-10, -30], [-36.66666666666667, -35.041], [-90, -45.123]],
|
|
27
33
|
);
|
|
28
34
|
});
|
|
35
|
+
|
|
29
36
|
it('last item should be our exact endpoint', () => {
|
|
30
37
|
expect(getCoordinatesBetween([0, 0], [100, -180], 3)).toStrictEqual(
|
|
31
38
|
[[0, 0], [33.333333333333336, -60], [100, -180]],
|
|
32
39
|
);
|
|
33
40
|
});
|
|
41
|
+
|
|
34
42
|
it('should handle long decimals', () => {
|
|
35
43
|
expect(getCoordinatesBetween([0.123, 0.999], [100.123, 101.666], 3)).toStrictEqual(
|
|
36
44
|
[[0.123, 0.999], [33.45633333333333, 34.55466666666667], [100.123, 101.666]],
|
|
37
45
|
);
|
|
38
46
|
});
|
|
39
47
|
});
|
|
48
|
+
|
|
49
|
+
describe('getLoansIntervals', () => {
|
|
50
|
+
it('should return array of length 1 if max number is smaller than number of interval', () => {
|
|
51
|
+
const result = getLoansIntervals(1, 5, 6);
|
|
52
|
+
expect(result.length).toStrictEqual(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should dont overlap results', () => {
|
|
56
|
+
const result = getLoansIntervals(1, 30, 6);
|
|
57
|
+
expect(result[0][1]).not.toBe(result[1][0]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should have a difference by one between intervals', () => {
|
|
61
|
+
const result = getLoansIntervals(1, 120, 6);
|
|
62
|
+
expect(result[0][1]).toBe(result[1][0] - 1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('getCountryColor', () => {
|
|
67
|
+
it('should return #C4C4C4 gray as default color', () => {
|
|
68
|
+
const result = getCountryColor(0, mockLenderCountries, kvTokensPrimitives);
|
|
69
|
+
expect(result).toBe('#C4C4C4');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
40
72
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import kvTokensPrimitives from '@kiva/kv-tokens/primitives.json';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Code to generate random coordinates
|
|
3
5
|
* */
|
|
@@ -10,6 +12,18 @@ const randomCoordinates = Array.from(
|
|
|
10
12
|
() => [getRandomInRange(-180, 180, 3), getRandomInRange(-90, 90, 3)],
|
|
11
13
|
);
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Color indexes for the map
|
|
17
|
+
* */
|
|
18
|
+
const mapColors = [
|
|
19
|
+
100,
|
|
20
|
+
300,
|
|
21
|
+
500,
|
|
22
|
+
650,
|
|
23
|
+
800,
|
|
24
|
+
1000,
|
|
25
|
+
];
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Given 2 coordinates and the number of steps return an array of coordinates in between
|
|
15
29
|
* @param {Array} startCoordinates - starting coordinates in the format [latitude, longitude]
|
|
@@ -270,3 +284,79 @@ export function animationCoordinator(mapInstance, borrowerPoints) {
|
|
|
270
284
|
flyToPoint(currentPointIndex);
|
|
271
285
|
});
|
|
272
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* This function returns an array of not overlapped intervals between min and max
|
|
290
|
+
* @param {Integer} min - min number of the interval
|
|
291
|
+
* @param {Integer} max - max number of the interval
|
|
292
|
+
* @param {Integer} nbIntervals - number of intervals
|
|
293
|
+
* @returns {Array} - array with intervals
|
|
294
|
+
* */
|
|
295
|
+
export const getLoansIntervals = (min, max, nbIntervals) => {
|
|
296
|
+
const size = Math.floor((max - min) / nbIntervals);
|
|
297
|
+
const result = [];
|
|
298
|
+
|
|
299
|
+
if (size <= 0) return [[min, max]];
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < nbIntervals; i += 1) {
|
|
302
|
+
let inf = min + (i * size);
|
|
303
|
+
let sup = ((inf + size) < max) ? inf + size : max;
|
|
304
|
+
|
|
305
|
+
if (i > 0) {
|
|
306
|
+
inf += (1 * i);
|
|
307
|
+
sup += (1 * i);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (i > 0 && sup > max) {
|
|
311
|
+
sup = max;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (i === (nbIntervals - 1)) {
|
|
315
|
+
if (sup < max || sup > max) sup = max;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
result.push([inf, sup]);
|
|
319
|
+
|
|
320
|
+
if (sup >= max) break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return result;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* This function returns the color of the country based on the number of loans
|
|
328
|
+
* @param {Integer} lenderLoans - number of loans per country
|
|
329
|
+
* @param {Array} countriesData - data of countries
|
|
330
|
+
* @param {Object} kvTokensPrimitives - kv tokens for colors
|
|
331
|
+
* @returns {String} - color of the country
|
|
332
|
+
* */
|
|
333
|
+
export const getCountryColor = (lenderLoans, countriesData) => {
|
|
334
|
+
const loanCountsArray = [];
|
|
335
|
+
countriesData.forEach((country) => {
|
|
336
|
+
loanCountsArray.push(country.value);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const maxNumLoansToOneCountry = Math.max(...loanCountsArray);
|
|
340
|
+
const intervals = getLoansIntervals(1, maxNumLoansToOneCountry, 6);
|
|
341
|
+
|
|
342
|
+
if (intervals.length === 1) {
|
|
343
|
+
const [inf, sup] = intervals[0]; // eslint-disable-line no-unused-vars
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < sup; i += 1) {
|
|
346
|
+
const loansNumber = i + 1;
|
|
347
|
+
|
|
348
|
+
if (lenderLoans && lenderLoans >= loansNumber && lenderLoans < loansNumber + 1) {
|
|
349
|
+
return kvTokensPrimitives.colors.brand[mapColors[i]];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
for (let i = 0; i < intervals.length; i += 1) {
|
|
354
|
+
const [inf, sup] = intervals[i];
|
|
355
|
+
if (lenderLoans && lenderLoans >= inf && lenderLoans <= sup) {
|
|
356
|
+
return kvTokensPrimitives.colors.brand[mapColors[i]];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return kvTokensPrimitives.colors.gray[300];
|
|
362
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="
|
|
3
|
+
class="tw-flex tw-flex-col tw-bg-white tw-rounded tw-w-full tw-pb-1"
|
|
4
4
|
:class="{ 'tw-pointer-events-none' : isLoading }"
|
|
5
5
|
data-testid="loan-card"
|
|
6
6
|
style="box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);"
|
|
@@ -382,9 +382,6 @@ export default {
|
|
|
382
382
|
</script>
|
|
383
383
|
|
|
384
384
|
<style lang="postcss" scoped>
|
|
385
|
-
.card-container {
|
|
386
|
-
@apply tw-flex tw-flex-col tw-bg-white tw-rounded tw-w-full tw-pb-1;
|
|
387
|
-
}
|
|
388
385
|
.loan-callouts >>> span {
|
|
389
386
|
@apply !tw-bg-transparent tw-text-brand;
|
|
390
387
|
}
|
package/vue/KvMap.vue
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<script>
|
|
16
|
-
import
|
|
16
|
+
import kvTokensPrimitives from '@kiva/kv-tokens/primitives.json';
|
|
17
|
+
import { animationCoordinator, generateMapMarkers, getCountryColor } from '../utils/mapUtils';
|
|
18
|
+
import countriesBorders from '../data/countries-borders.json';
|
|
17
19
|
|
|
18
20
|
export default {
|
|
19
21
|
name: 'KvMap',
|
|
@@ -115,6 +117,44 @@ export default {
|
|
|
115
117
|
required: false,
|
|
116
118
|
default: () => ({}),
|
|
117
119
|
},
|
|
120
|
+
/**
|
|
121
|
+
* Show the zoom control
|
|
122
|
+
*/
|
|
123
|
+
showZoomControl: {
|
|
124
|
+
type: Boolean,
|
|
125
|
+
default: false,
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* Allow dragging of the map
|
|
129
|
+
*/
|
|
130
|
+
allowDragging: {
|
|
131
|
+
type: Boolean,
|
|
132
|
+
default: false,
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Show labels on the map
|
|
136
|
+
* Working for leaflet only
|
|
137
|
+
*/
|
|
138
|
+
showLabels: {
|
|
139
|
+
type: Boolean,
|
|
140
|
+
default: true,
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Lender data for the map
|
|
144
|
+
* Working for leaflet only
|
|
145
|
+
*/
|
|
146
|
+
countriesData: {
|
|
147
|
+
type: Array,
|
|
148
|
+
default: () => ([]),
|
|
149
|
+
},
|
|
150
|
+
/**
|
|
151
|
+
* Show fundraising loans
|
|
152
|
+
* Working for leaflet only
|
|
153
|
+
*/
|
|
154
|
+
showFundraisingLoans: {
|
|
155
|
+
type: Boolean,
|
|
156
|
+
default: false,
|
|
157
|
+
},
|
|
118
158
|
},
|
|
119
159
|
data() {
|
|
120
160
|
return {
|
|
@@ -154,6 +194,12 @@ export default {
|
|
|
154
194
|
this.initializeMap();
|
|
155
195
|
}
|
|
156
196
|
},
|
|
197
|
+
showFundraisingLoans() {
|
|
198
|
+
if (this.mapInstance) {
|
|
199
|
+
this.mapInstance.remove();
|
|
200
|
+
this.initializeLeaflet();
|
|
201
|
+
}
|
|
202
|
+
},
|
|
157
203
|
},
|
|
158
204
|
mounted() {
|
|
159
205
|
if (!this.mapLibreReady && !this.leafletReady) {
|
|
@@ -287,8 +333,8 @@ export default {
|
|
|
287
333
|
center: [this.lat, this.long],
|
|
288
334
|
zoom: this.initialZoom || this.zoomLevel,
|
|
289
335
|
// todo make props for the following options
|
|
290
|
-
dragging:
|
|
291
|
-
zoomControl:
|
|
336
|
+
dragging: this.allowDragging,
|
|
337
|
+
zoomControl: this.showZoomControl,
|
|
292
338
|
animate: true,
|
|
293
339
|
scrollWheelZoom: false,
|
|
294
340
|
doubleClickZoom: false,
|
|
@@ -296,12 +342,45 @@ export default {
|
|
|
296
342
|
});
|
|
297
343
|
/* eslint-disable quotes */
|
|
298
344
|
// Add our tileset to the mapInstance
|
|
299
|
-
|
|
345
|
+
let tileLayer = 'https://api.maptiler.com/maps/landscape/{z}/{x}/{y}.png?key=n1Mz5ziX3k6JfdjFe7mx';
|
|
346
|
+
if (this.showLabels) {
|
|
347
|
+
tileLayer = 'https://api.maptiler.com/maps/bright/{z}/{x}/{y}.png?key=n1Mz5ziX3k6JfdjFe7mx';
|
|
348
|
+
}
|
|
349
|
+
L.tileLayer(tileLayer, {
|
|
300
350
|
tileSize: 512,
|
|
301
351
|
zoomOffset: -1,
|
|
302
352
|
minZoom: 1,
|
|
303
353
|
crossOrigin: true,
|
|
304
354
|
}).addTo(this.mapInstance);
|
|
355
|
+
|
|
356
|
+
if (this.countriesData.length > 0) {
|
|
357
|
+
L.geoJson(
|
|
358
|
+
this.getCountriesData(),
|
|
359
|
+
{
|
|
360
|
+
style: this.countryStyle,
|
|
361
|
+
onEachFeature: this.onEachCountryFeature,
|
|
362
|
+
},
|
|
363
|
+
).addTo(this.mapInstance);
|
|
364
|
+
|
|
365
|
+
this.countriesData.forEach((country) => {
|
|
366
|
+
if (country.numLoansFundraising > 0 && this.showFundraisingLoans) {
|
|
367
|
+
const circle = L.circle([country.lat, country.long], {
|
|
368
|
+
color: kvTokensPrimitives.colors.black,
|
|
369
|
+
weight: 1,
|
|
370
|
+
fillColor: kvTokensPrimitives.colors.brand[900],
|
|
371
|
+
fillOpacity: 1,
|
|
372
|
+
radius: 130000,
|
|
373
|
+
}).addTo(this.mapInstance);
|
|
374
|
+
|
|
375
|
+
const tooltipText = `Click to see ${country.numLoansFundraising} fundraising loans in ${country.label}`;
|
|
376
|
+
circle.bindTooltip(tooltipText);
|
|
377
|
+
|
|
378
|
+
circle.on('click', () => {
|
|
379
|
+
this.circleMapClicked(country.isoCode);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
305
384
|
/* eslint-enable quotes */
|
|
306
385
|
/* eslint-enable no-undef, max-len */
|
|
307
386
|
|
|
@@ -314,19 +393,28 @@ export default {
|
|
|
314
393
|
},
|
|
315
394
|
initializeMapLibre() {
|
|
316
395
|
// Initialize primary mapInstance
|
|
317
|
-
|
|
396
|
+
/* eslint-disable no-undef */
|
|
397
|
+
let tileLayer = 'https://api.maptiler.com/maps/landscape/style.json?key=n1Mz5ziX3k6JfdjFe7mx';
|
|
398
|
+
if (this.showLabels) {
|
|
399
|
+
tileLayer = 'https://api.maptiler.com/maps/bright/style.json?key=n1Mz5ziX3k6JfdjFe7mx';
|
|
400
|
+
}
|
|
401
|
+
|
|
318
402
|
this.mapInstance = new maplibregl.Map({
|
|
319
403
|
container: `kv-map-holder-${this.mapId}`,
|
|
320
|
-
style:
|
|
404
|
+
style: tileLayer,
|
|
321
405
|
center: [this.long, this.lat],
|
|
322
406
|
zoom: this.initialZoom || this.zoomLevel,
|
|
323
407
|
attributionControl: false,
|
|
324
|
-
dragPan:
|
|
408
|
+
dragPan: this.allowDragging,
|
|
325
409
|
scrollZoom: false,
|
|
326
410
|
doubleClickZoom: false,
|
|
327
411
|
dragRotate: false,
|
|
328
412
|
});
|
|
329
413
|
|
|
414
|
+
if (this.showZoomControl) {
|
|
415
|
+
this.mapInstance.addControl(new maplibregl.NavigationControl());
|
|
416
|
+
}
|
|
417
|
+
|
|
330
418
|
this.mapInstance.on('load', () => {
|
|
331
419
|
// signify map has loaded
|
|
332
420
|
this.mapLoaded = true;
|
|
@@ -335,6 +423,7 @@ export default {
|
|
|
335
423
|
this.createWrapperObserver();
|
|
336
424
|
}
|
|
337
425
|
});
|
|
426
|
+
/* eslint-enable no-undef */
|
|
338
427
|
},
|
|
339
428
|
animateMap() {
|
|
340
429
|
// remove country labels
|
|
@@ -408,6 +497,60 @@ export default {
|
|
|
408
497
|
}, timeout);
|
|
409
498
|
});
|
|
410
499
|
},
|
|
500
|
+
getCountriesData() {
|
|
501
|
+
const countriesFeatures = countriesBorders.features ?? [];
|
|
502
|
+
|
|
503
|
+
countriesFeatures.forEach((country, index) => {
|
|
504
|
+
const countryData = this.countriesData.find((data) => data.isoCode === country.properties.ISO_A2);
|
|
505
|
+
if (countryData) {
|
|
506
|
+
countriesFeatures[index].lenderLoans = countryData.value;
|
|
507
|
+
countriesFeatures[index].numLoansFundraising = countryData.numLoansFundraising;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
return countriesBorders;
|
|
512
|
+
},
|
|
513
|
+
countryStyle(feature) {
|
|
514
|
+
return {
|
|
515
|
+
color: kvTokensPrimitives.colors.white,
|
|
516
|
+
fillColor: getCountryColor(feature.lenderLoans, this.countriesData),
|
|
517
|
+
weight: 1,
|
|
518
|
+
fillOpacity: 1,
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
onEachCountryFeature(feature, layer) {
|
|
522
|
+
const loansString = feature.lenderLoans
|
|
523
|
+
? `${feature.lenderLoans} loan${feature.lenderLoans > 1 ? 's' : ''}`
|
|
524
|
+
: '0 loans';
|
|
525
|
+
const countryString = `${feature.properties.NAME} <br/> ${loansString}`;
|
|
526
|
+
|
|
527
|
+
layer.bindTooltip(countryString, {
|
|
528
|
+
sticky: true,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
layer.on({
|
|
532
|
+
mouseover: this.highlightFeature,
|
|
533
|
+
mouseout: this.resetHighlight,
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
highlightFeature(e) {
|
|
537
|
+
const layer = e.target;
|
|
538
|
+
|
|
539
|
+
layer.setStyle({
|
|
540
|
+
fillColor: kvTokensPrimitives.colors.gray[500],
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
resetHighlight(e) {
|
|
544
|
+
const layer = e.target;
|
|
545
|
+
const { feature } = layer;
|
|
546
|
+
|
|
547
|
+
layer.setStyle({
|
|
548
|
+
fillColor: getCountryColor(feature.lenderLoans, this.countriesData),
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
circleMapClicked(countryIso) {
|
|
552
|
+
this.$emit('country-lend-filter', countryIso);
|
|
553
|
+
},
|
|
411
554
|
},
|
|
412
555
|
};
|
|
413
556
|
</script>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import mockLenderCountries from '../../tests/fixtures/mockLenderCountries';
|
|
1
2
|
import KvMap from '../KvMap.vue';
|
|
2
3
|
|
|
3
4
|
export default {
|
|
@@ -32,6 +33,11 @@ const Template = (args, { argTypes }) => ({
|
|
|
32
33
|
:width="width"
|
|
33
34
|
:zoom-level="zoomLevel"
|
|
34
35
|
:advanced-animation="advancedAnimation"
|
|
36
|
+
:show-zoom-control="showZoomControl"
|
|
37
|
+
:allow-dragging="allowDragging"
|
|
38
|
+
:show-labels="showLabels"
|
|
39
|
+
:countries-data="countriesData"
|
|
40
|
+
:show-fundraising-loans="showFundraisingLoans"
|
|
35
41
|
/>`,
|
|
36
42
|
});
|
|
37
43
|
|
|
@@ -98,3 +104,18 @@ AdvancedAnimation.args = {
|
|
|
98
104
|
long: -31.690,
|
|
99
105
|
advancedAnimation,
|
|
100
106
|
};
|
|
107
|
+
|
|
108
|
+
export const LoansMap = Template.bind({});
|
|
109
|
+
LoansMap.args = {
|
|
110
|
+
autoZoomDelay: 500,
|
|
111
|
+
aspectRatio: 1.8,
|
|
112
|
+
lat: 30,
|
|
113
|
+
long: 1,
|
|
114
|
+
zoomLevel: 2,
|
|
115
|
+
useLeaflet: true,
|
|
116
|
+
showZoomControl: true,
|
|
117
|
+
allowDragging: true,
|
|
118
|
+
showLabels: false,
|
|
119
|
+
countriesData: mockLenderCountries,
|
|
120
|
+
showFundraisingLoans: true,
|
|
121
|
+
};
|