@kiva/kv-components 3.90.5 → 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.
@@ -1,8 +1,12 @@
1
+ import kvTokensPrimitives from '@kiva/kv-tokens/primitives.json';
1
2
  import {
2
3
  getCoordinatesBetween,
3
- } from '../../../../utils/mapAnimation';
4
+ getLoansIntervals,
5
+ getCountryColor,
6
+ } from '../../../../utils/mapUtils';
7
+ import mockLenderCountries from '../../../fixtures/mockLenderCountries';
4
8
 
5
- describe('mapAnimation', () => {
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
+ };
package/vue/KvMap.vue CHANGED
@@ -13,7 +13,9 @@
13
13
  </template>
14
14
 
15
15
  <script>
16
- import { animationCoordinator, generateMapMarkers } from '../utils/mapAnimation';
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: false,
291
- zoomControl: false,
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
- L.tileLayer('https://api.maptiler.com/maps/bright/{z}/{x}/{y}.png?key=n1Mz5ziX3k6JfdjFe7mx', {
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
- // eslint-disable-next-line no-undef
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: 'https://api.maptiler.com/maps/bright/style.json?key=n1Mz5ziX3k6JfdjFe7mx',
404
+ style: tileLayer,
321
405
  center: [this.long, this.lat],
322
406
  zoom: this.initialZoom || this.zoomLevel,
323
407
  attributionControl: false,
324
- dragPan: false,
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
+ };