@kiva/kv-components 3.90.5 → 3.91.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.
@@ -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
+ };
@@ -217,16 +217,23 @@ export default {
217
217
  const kvLightbox = ref(null);
218
218
  const kvLightboxBody = ref(null);
219
219
  const controlsRef = ref(null);
220
+ const activateFocusTrap = ref(null);
221
+ const deactivateFocusTrap = ref(null);
220
222
 
221
- const trapElements = computed(() => [
222
- kvLightbox.value, // This lightbox
223
- '[role="alert"]', // Any open toasts/alerts on the page
224
- ]);
225
- const {
226
- activate: activateFocusTrap,
227
- deactivate: deactivateFocusTrap,
228
- } = useFocusTrap(trapElements, {
229
- allowOutsideClick: true, // allow clicking outside the lightbox to close it
223
+ // Ensure the lightbox ref isn't null
224
+ nextTick(() => {
225
+ const trapElements = computed(() => [
226
+ kvLightbox.value, // This lightbox
227
+ '[role="alert"]', // Any open toasts/alerts on the page
228
+ ]);
229
+ const {
230
+ activate,
231
+ deactivate,
232
+ } = useFocusTrap(trapElements, {
233
+ allowOutsideClick: true, // allow clicking outside the lightbox to close it
234
+ });
235
+ activateFocusTrap.value = activate;
236
+ deactivateFocusTrap.value = deactivate;
230
237
  });
231
238
 
232
239
  let makePageInertCallback = null;
@@ -242,7 +249,7 @@ export default {
242
249
  const hide = (closedBy = '') => {
243
250
  // scroll any content inside the lightbox back to top
244
251
  if (kvLightbox.value && kvLightboxBody.value) {
245
- deactivateFocusTrap();
252
+ deactivateFocusTrap.value?.();
246
253
  kvLightboxBody.value.scrollTop = 0;
247
254
  unlockPrintSingleEl(kvLightboxBody.value);
248
255
  }
@@ -279,7 +286,7 @@ export default {
279
286
 
280
287
  nextTick(() => {
281
288
  if (kvLightbox.value && kvLightboxBody.value) {
282
- activateFocusTrap();
289
+ activateFocusTrap.value?.();
283
290
  makePageInertCallback = makePageInert(kvLightbox.value);
284
291
  lockPrintSingleEl(kvLightboxBody.value);
285
292
  }
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
+ };