@kiva/kv-components 3.93.0 → 3.95.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 +32 -0
- package/package.json +4 -2
- package/utils/Alea.js +92 -0
- package/utils/loanCard.js +4 -1
- package/utils/touchEvents.js +22 -0
- package/vue/KvIntroductionLoanCard.vue +41 -26
- package/vue/KvLoanTag.vue +20 -5
- package/vue/KvPieChart.vue +232 -0
- package/vue/KvPopper.vue +178 -0
- package/vue/KvTooltip.vue +168 -0
- package/vue/KvTreeMapChart.vue +229 -0
- package/vue/stories/KvClassicLoanCard.stories.js +21 -0
- package/vue/stories/KvIntroductionLoanCard.stories.js +78 -11
- package/vue/stories/KvPieChart.stories.js +42 -0
- package/vue/stories/KvTooltip.stories.js +26 -0
- package/vue/stories/KvTreeMapChart.stories.js +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [3.95.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.94.0...@kiva/kv-components@3.95.0) (2024-08-28)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* add story for long borrower name ([da58fc9](https://github.com/kiva/kv-ui-elements/commit/da58fc9a078626577370eea41a0d9336edc2c74e))
|
|
12
|
+
* allow new card to be pull width ([59488dc](https://github.com/kiva/kv-ui-elements/commit/59488dc7e9550d8df38f73fe0b859844e65c6576))
|
|
13
|
+
* fully funded card ([17a9156](https://github.com/kiva/kv-ui-elements/commit/17a9156fc8003f2a19feb030cfe1e1ba369222dc))
|
|
14
|
+
* general new loan card styling tweaks ([dac3bb1](https://github.com/kiva/kv-ui-elements/commit/dac3bb14d62c97b04e7979e0c512edc4c90634c8))
|
|
15
|
+
* hide country for US loans ([5353e0f](https://github.com/kiva/kv-ui-elements/commit/5353e0f9b1678d013becf7775bb4c408cf72887a))
|
|
16
|
+
* one more small styling change ([042da40](https://github.com/kiva/kv-ui-elements/commit/042da402e089805260f4e465c8d237baf6bfe642))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* add emojis and colors to loan tags ([8d002c2](https://github.com/kiva/kv-ui-elements/commit/8d002c25a243c305fae5113305a604c2f34746e4))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# [3.94.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.93.0...@kiva/kv-components@3.94.0) (2024-08-23)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* charts added to library ([#447](https://github.com/kiva/kv-ui-elements/issues/447)) ([575bd80](https://github.com/kiva/kv-ui-elements/commit/575bd80fb1700a38732a2b8cafbeb5cc5374764b))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
6
38
|
# [3.93.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.92.1...@kiva/kv-components@3.93.0) (2024-08-22)
|
|
7
39
|
|
|
8
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kiva/kv-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.95.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
"moment": "^2.29.4",
|
|
72
72
|
"nanoid": "^3.1.23",
|
|
73
73
|
"numeral": "^2.0.6",
|
|
74
|
+
"popper.js": "^1.16.1",
|
|
75
|
+
"treemap-squarify": "^1.0.1",
|
|
74
76
|
"vue-demi": "^0.14.7"
|
|
75
77
|
},
|
|
76
78
|
"peerDependencies": {
|
|
@@ -82,5 +84,5 @@
|
|
|
82
84
|
"optional": true
|
|
83
85
|
}
|
|
84
86
|
},
|
|
85
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "5f8bc910fcf78e8ea410172056d9172c55dc80c0"
|
|
86
88
|
}
|
package/utils/Alea.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* eslint-disable no-plusplus, no-param-reassign, no-return-assign, no-bitwise, prefer-rest-params, func-names */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The hash function used for Alea pseudo random number generator
|
|
5
|
+
*
|
|
6
|
+
* Johannes Baagøe <baagoe@baagoe.com>, 2010
|
|
7
|
+
* Licensed under the MIT license
|
|
8
|
+
*
|
|
9
|
+
* {@link https://github.com/nquinlan/better-random-numbers-for-javascript-mirror}
|
|
10
|
+
*
|
|
11
|
+
* @returns The hash of the provided data
|
|
12
|
+
*/
|
|
13
|
+
export function Mash() {
|
|
14
|
+
let n = 0xefc8249d;
|
|
15
|
+
|
|
16
|
+
const mash = function (data) {
|
|
17
|
+
data = data.toString();
|
|
18
|
+
for (let i = 0; i < data.length; i++) {
|
|
19
|
+
n += data.charCodeAt(i);
|
|
20
|
+
let h = 0.02519603282416938 * n;
|
|
21
|
+
n = h >>> 0;
|
|
22
|
+
h -= n;
|
|
23
|
+
h *= n;
|
|
24
|
+
n = h >>> 0;
|
|
25
|
+
h -= n;
|
|
26
|
+
n += h * 0x100000000; // 2^32
|
|
27
|
+
}
|
|
28
|
+
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
mash.version = 'Mash 0.9';
|
|
32
|
+
|
|
33
|
+
return mash;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pseudo random number generator that returns a repeatable set of random numbers when supplied matching seeds
|
|
38
|
+
*
|
|
39
|
+
* Johannes Baagøe <baagoe@baagoe.com>, 2010
|
|
40
|
+
* Licensed under the MIT license
|
|
41
|
+
*
|
|
42
|
+
* {@link https://github.com/nquinlan/better-random-numbers-for-javascript-mirror}
|
|
43
|
+
*
|
|
44
|
+
* @returns The seeded generator function
|
|
45
|
+
*/
|
|
46
|
+
export default function Alea() {
|
|
47
|
+
return (function (args) {
|
|
48
|
+
let s0 = 0;
|
|
49
|
+
let s1 = 0;
|
|
50
|
+
let s2 = 0;
|
|
51
|
+
let c = 1;
|
|
52
|
+
|
|
53
|
+
if (args.length === 0) {
|
|
54
|
+
args = [+new Date()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let mash = Mash();
|
|
58
|
+
s0 = mash(' ');
|
|
59
|
+
s1 = mash(' ');
|
|
60
|
+
s2 = mash(' ');
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < args.length; i++) {
|
|
63
|
+
s0 -= mash(args[i]);
|
|
64
|
+
if (s0 < 0) {
|
|
65
|
+
s0 += 1;
|
|
66
|
+
}
|
|
67
|
+
s1 -= mash(args[i]);
|
|
68
|
+
if (s1 < 0) {
|
|
69
|
+
s1 += 1;
|
|
70
|
+
}
|
|
71
|
+
s2 -= mash(args[i]);
|
|
72
|
+
if (s2 < 0) {
|
|
73
|
+
s2 += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
mash = null;
|
|
78
|
+
|
|
79
|
+
const random = function () {
|
|
80
|
+
const t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
|
|
81
|
+
s0 = s1;
|
|
82
|
+
s1 = s2;
|
|
83
|
+
return s2 = t - (c = t | 0);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
random.version = 'Alea 0.9';
|
|
87
|
+
|
|
88
|
+
random.args = args;
|
|
89
|
+
|
|
90
|
+
return random;
|
|
91
|
+
}(Array.prototype.slice.call(arguments)));
|
|
92
|
+
}
|
package/utils/loanCard.js
CHANGED
|
@@ -21,6 +21,7 @@ export function loanCardComputedProperties(props) {
|
|
|
21
21
|
loan,
|
|
22
22
|
categoryPageName,
|
|
23
23
|
customCallouts,
|
|
24
|
+
hideUnitedStatesText,
|
|
24
25
|
} = toRefs(props);
|
|
25
26
|
|
|
26
27
|
const tag = computed(() => (externalLinks.value ? 'a' : 'router-link'));
|
|
@@ -49,7 +50,9 @@ export function loanCardComputedProperties(props) {
|
|
|
49
50
|
|
|
50
51
|
const formattedLocation = computed(() => {
|
|
51
52
|
if (distributionModel.value === 'direct') {
|
|
52
|
-
|
|
53
|
+
const countryText = hideUnitedStatesText?.value && countryName.value.toLowerCase() === 'united states'
|
|
54
|
+
? '' : `, ${countryName.value}`;
|
|
55
|
+
return `${city.value}, ${state.value}${countryText}`;
|
|
53
56
|
}
|
|
54
57
|
if (countryName.value === 'Puerto Rico') {
|
|
55
58
|
return `${city.value}, PR`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Attach a touchstart event handler to the immediate children of the body.
|
|
2
|
+
// Useful for capturing the user tapping outside of a target element.
|
|
3
|
+
export function onBodyTouchstart(handler) {
|
|
4
|
+
[...document.body.children].forEach((child) => child.addEventListener('touchstart', handler));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Remove a touchstart event handler from the immediate children of the body
|
|
8
|
+
export function offBodyTouchstart(handler) {
|
|
9
|
+
[...document.body.children].forEach((child) => child.removeEventListener('touchstart', handler));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Returns true if the event target is any of the given elements or if the event target
|
|
13
|
+
// is contained by any of the given elements.
|
|
14
|
+
export function isTargetElement(event, elements) {
|
|
15
|
+
const els = Array.isArray(elements) ? elements : [elements];
|
|
16
|
+
for (let i = 0; i < els.length; i += 1) {
|
|
17
|
+
if (els[i] === event.target || els[i].contains(event.target)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="tw-flex tw-flex-col tw-bg-white tw-rounded tw-w-full tw-pb-
|
|
3
|
+
class="tw-flex tw-flex-col tw-bg-white tw-rounded tw-w-full tw-pb-2"
|
|
4
4
|
:class="{ 'tw-pointer-events-none' : isLoading }"
|
|
5
5
|
data-testid="loan-card"
|
|
6
|
-
style="box-shadow:
|
|
7
|
-
:style="{ minWidth: '230px', maxWidth:
|
|
6
|
+
style="box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.38);"
|
|
7
|
+
:style="{ minWidth: '230px', maxWidth: cardWidth }"
|
|
8
8
|
>
|
|
9
9
|
<div class="tw-grow">
|
|
10
10
|
<div class="loan-card-active-hover">
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
style="padding: 2px 6px;"
|
|
64
64
|
>
|
|
65
65
|
<kv-flag
|
|
66
|
-
class="tw-
|
|
66
|
+
class="tw-ml-0.5 tw-mr-1"
|
|
67
67
|
:country="countryCode"
|
|
68
|
-
width-override="
|
|
68
|
+
width-override="16px"
|
|
69
69
|
hide-border
|
|
70
70
|
/>
|
|
71
71
|
{{ formattedLocation }}
|
|
@@ -82,13 +82,14 @@
|
|
|
82
82
|
:style="{ width: '60%', height: '1.75rem', 'border-radius': '500rem' }"
|
|
83
83
|
/>
|
|
84
84
|
|
|
85
|
-
<
|
|
85
|
+
<h3
|
|
86
86
|
class="loan-card-name"
|
|
87
|
-
:class="{'tw-text-center': borrowerName.length < 20}"
|
|
87
|
+
:class="{ 'tw-text-center': borrowerName.length < 20 }"
|
|
88
|
+
style="font-size: 28px;"
|
|
88
89
|
@click="clickReadMore('Name', $event)"
|
|
89
90
|
>
|
|
90
91
|
{{ borrowerName }}
|
|
91
|
-
</
|
|
92
|
+
</h3>
|
|
92
93
|
|
|
93
94
|
<!-- Loan tag -->
|
|
94
95
|
|
|
@@ -99,17 +100,17 @@
|
|
|
99
100
|
<kv-loading-placeholder
|
|
100
101
|
v-if="isLoading || typeof loanCallouts === 'undefined'"
|
|
101
102
|
class="tw-mt-0.5 tw-mb-1"
|
|
102
|
-
:style="{ width: '20%', height: '1.
|
|
103
|
+
:style="{ width: '20%', height: '1.65rem', 'border-radius': '500rem' }"
|
|
103
104
|
/>
|
|
104
105
|
<kv-loading-placeholder
|
|
105
106
|
v-if="isLoading || typeof loanCallouts === 'undefined'"
|
|
106
107
|
class="tw-mt-0.5 tw-mb-1"
|
|
107
|
-
:style="{ width: '20%', height: '1.
|
|
108
|
+
:style="{ width: '20%', height: '1.65rem', 'border-radius': '500rem' }"
|
|
108
109
|
/>
|
|
109
110
|
<kv-loading-placeholder
|
|
110
111
|
v-if="isLoading || typeof loanCallouts === 'undefined'"
|
|
111
112
|
class="tw-mt-0.5 tw-mb-1"
|
|
112
|
-
:style="{ width: '20%', height: '1.
|
|
113
|
+
:style="{ width: '20%', height: '1.65rem', 'border-radius': '500rem' }"
|
|
113
114
|
/>
|
|
114
115
|
</div>
|
|
115
116
|
|
|
@@ -139,7 +140,7 @@
|
|
|
139
140
|
>
|
|
140
141
|
<!-- Loan use -->
|
|
141
142
|
<div
|
|
142
|
-
class="tw-pt-
|
|
143
|
+
class="tw-pt-0.5 tw-px-2"
|
|
143
144
|
:class="{'tw-mb-1.5': !isLoading}"
|
|
144
145
|
>
|
|
145
146
|
<div
|
|
@@ -172,7 +173,7 @@
|
|
|
172
173
|
</div>
|
|
173
174
|
</div>
|
|
174
175
|
|
|
175
|
-
<div class="tw-px-
|
|
176
|
+
<div class="tw-px-2">
|
|
176
177
|
<!-- Fundraising -->
|
|
177
178
|
<div
|
|
178
179
|
v-if="!hasProgressData"
|
|
@@ -215,21 +216,28 @@
|
|
|
215
216
|
</component>
|
|
216
217
|
</div>
|
|
217
218
|
|
|
218
|
-
<!-- Loan
|
|
219
|
+
<!-- Loan Tag -->
|
|
219
220
|
<kv-loading-placeholder
|
|
220
221
|
v-if="isLoading"
|
|
221
|
-
class="tw-rounded tw-mx-auto tw-mt-
|
|
222
|
+
class="tw-rounded tw-mx-auto tw-mt-2"
|
|
222
223
|
:style="{ width: '9rem', height: '1rem' }"
|
|
223
224
|
/>
|
|
224
225
|
|
|
225
|
-
<div
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
/>
|
|
226
|
+
<div
|
|
227
|
+
v-else-if="isFunded"
|
|
228
|
+
class="tw-bg-eco-green-1 tw-text-action tw-text-center tw-py-1.5"
|
|
229
|
+
style="font-weight: bold; font-size: 13px; border-radius: 8px;"
|
|
230
|
+
>
|
|
231
|
+
🎉 Fully funded!
|
|
232
232
|
</div>
|
|
233
|
+
|
|
234
|
+
<kv-loan-tag
|
|
235
|
+
v-else
|
|
236
|
+
:loan="loan"
|
|
237
|
+
:use-expanded-styles="true"
|
|
238
|
+
style="font-size: 15px;"
|
|
239
|
+
class="tw-text-center tw-pt-2"
|
|
240
|
+
/>
|
|
233
241
|
</div>
|
|
234
242
|
</div>
|
|
235
243
|
</template>
|
|
@@ -300,6 +308,10 @@ export default {
|
|
|
300
308
|
type: Object,
|
|
301
309
|
default: undefined,
|
|
302
310
|
},
|
|
311
|
+
useFullWidth: {
|
|
312
|
+
type: Boolean,
|
|
313
|
+
default: false,
|
|
314
|
+
},
|
|
303
315
|
},
|
|
304
316
|
setup(props, { emit }) {
|
|
305
317
|
const {
|
|
@@ -324,7 +336,7 @@ export default {
|
|
|
324
336
|
state,
|
|
325
337
|
tag,
|
|
326
338
|
unreservedAmount,
|
|
327
|
-
} = loanCardComputedProperties(props);
|
|
339
|
+
} = loanCardComputedProperties({ ...props, hideUnitedStatesText: true });
|
|
328
340
|
|
|
329
341
|
const {
|
|
330
342
|
clickReadMore,
|
|
@@ -356,6 +368,9 @@ export default {
|
|
|
356
368
|
};
|
|
357
369
|
},
|
|
358
370
|
computed: {
|
|
371
|
+
cardWidth() {
|
|
372
|
+
return this.useFullWidth ? '100%' : '20.5rem';
|
|
373
|
+
},
|
|
359
374
|
imageAspectRatio() {
|
|
360
375
|
return 5 / 8;
|
|
361
376
|
},
|
|
@@ -374,8 +389,8 @@ export default {
|
|
|
374
389
|
const amount = this.loan?.loanFundraisingInfo?.fundedAmount ?? 0;
|
|
375
390
|
return numeral(parseFloat(amount)).format('$0,0');
|
|
376
391
|
},
|
|
377
|
-
|
|
378
|
-
return this.loan?.
|
|
392
|
+
isFunded() {
|
|
393
|
+
return this.loan?.status === 'funded' || parseFloat(this.unreservedAmount) === 0;
|
|
379
394
|
},
|
|
380
395
|
},
|
|
381
396
|
};
|
|
@@ -383,7 +398,7 @@ export default {
|
|
|
383
398
|
|
|
384
399
|
<style lang="postcss" scoped>
|
|
385
400
|
.loan-callouts >>> span {
|
|
386
|
-
@apply !tw-bg-transparent tw-text-
|
|
401
|
+
@apply !tw-bg-transparent tw-text-action;
|
|
387
402
|
}
|
|
388
403
|
.loan-card-use:hover,
|
|
389
404
|
.loan-card-use:focus {
|
package/vue/KvLoanTag.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div
|
|
3
3
|
v-if="!!variation"
|
|
4
4
|
class="tw-text-small tw-font-medium tw-pt-0.5 tw-line-clamp-1"
|
|
5
|
-
style="color:
|
|
5
|
+
:style="{ color: tagColor }"
|
|
6
6
|
>
|
|
7
7
|
{{ tagText }}
|
|
8
8
|
<kv-countdown-timer
|
|
@@ -29,6 +29,10 @@ export default {
|
|
|
29
29
|
type: Object,
|
|
30
30
|
required: true,
|
|
31
31
|
},
|
|
32
|
+
useExpandedStyles: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
32
36
|
},
|
|
33
37
|
data() {
|
|
34
38
|
return {
|
|
@@ -59,11 +63,22 @@ export default {
|
|
|
59
63
|
},
|
|
60
64
|
tagText() {
|
|
61
65
|
switch (this.variation) {
|
|
62
|
-
case 'lse-loan': return 'High community impact
|
|
63
|
-
case 'almost-funded': return 'Almost funded
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
case 'lse-loan': return `${this.useExpandedStyles ? '⚡ ' : ''}High community impact`;
|
|
67
|
+
case 'almost-funded': return `${this.useExpandedStyles ? '💸 ' : ''}Almost funded`;
|
|
68
|
+
// eslint-disable-next-line max-len
|
|
69
|
+
case 'matched-loan': return `${this.useExpandedStyles ? '🤝 ' : ''}${this.matchRatio + 1}x matching by ${this.loan?.matchingText}`;
|
|
70
|
+
default: return `${this.useExpandedStyles ? '⏰ ' : ''}Ending soon: `;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
tagColor() {
|
|
74
|
+
if (this.useExpandedStyles) {
|
|
75
|
+
switch (this.variation) {
|
|
76
|
+
case 'almost-funded': return '#AF741C';
|
|
77
|
+
case 'ending-soon': return '#CE2626';
|
|
78
|
+
default: return '#2B7C5F';
|
|
79
|
+
}
|
|
66
80
|
}
|
|
81
|
+
return '#CE4A00';
|
|
67
82
|
},
|
|
68
83
|
matchRatio() {
|
|
69
84
|
return this.loan?.matchRatio;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<figure
|
|
3
|
+
class="pie-chart tw-flex tw-flex-col md:tw-flex-row tw-items-center tw-justify-center tw-gap-2"
|
|
4
|
+
@mouseleave="activeSlice = null"
|
|
5
|
+
>
|
|
6
|
+
<!-- pie chart -->
|
|
7
|
+
<div class="tw-relative tw-h-full">
|
|
8
|
+
<div
|
|
9
|
+
v-if="loading"
|
|
10
|
+
class="pie-placeholder tw-h-full tw-p-2.5"
|
|
11
|
+
>
|
|
12
|
+
<div class="tw-overflow-hidden tw-rounded-full tw-h-full">
|
|
13
|
+
<kv-loading-placeholder />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<svg
|
|
17
|
+
v-else
|
|
18
|
+
class="tw-h-full"
|
|
19
|
+
viewBox="0 0 32 32"
|
|
20
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
22
|
+
>
|
|
23
|
+
<path
|
|
24
|
+
v-for="(slice, index) in slices"
|
|
25
|
+
:key="index"
|
|
26
|
+
class="tw-origin-center tw-transition-transform"
|
|
27
|
+
:style="activeSlice === slice ? { transform: 'scale(1.1)' } : {}"
|
|
28
|
+
:d="slice.path"
|
|
29
|
+
:stroke="slice.color"
|
|
30
|
+
:stroke-width="lineWidth"
|
|
31
|
+
fill="none"
|
|
32
|
+
@mouseenter="activeSlice = slice"
|
|
33
|
+
@click="activeSlice = slice"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
<!-- active slice -->
|
|
37
|
+
<div
|
|
38
|
+
v-if="activeSlice"
|
|
39
|
+
class="
|
|
40
|
+
tw-absolute tw-top-1/2 tw-left-1/2 -tw-translate-x-1/2 -tw-translate-y-1/2
|
|
41
|
+
tw-flex tw-flex-col tw-items-center tw-text-center"
|
|
42
|
+
style="max-width: 8.5rem;"
|
|
43
|
+
>
|
|
44
|
+
<div class="tw-font-medium tw-line-clamp-4">
|
|
45
|
+
{{ activeSlice.label }}
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
{{ activeSlice.value }} ({{ activeSlicePercent }})
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<!-- key -->
|
|
53
|
+
<div
|
|
54
|
+
style="width: 14rem; height: 85%;"
|
|
55
|
+
class="tw-flex tw-flex-col tw-justify-between"
|
|
56
|
+
>
|
|
57
|
+
<ol class="tw-pl-4 md:tw-pl-0">
|
|
58
|
+
<li
|
|
59
|
+
v-for="(slice, index) in slices.slice(pageIndex * slicesPerPage, (pageIndex + 1) * slicesPerPage)"
|
|
60
|
+
:key="index"
|
|
61
|
+
class="tw-flex tw-items-center"
|
|
62
|
+
@mouseenter="activeSlice = slice"
|
|
63
|
+
@click="activeSlice = slice"
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
class="tw-w-2 tw-h-2 tw-mr-1 tw-rounded-full tw-flex-none"
|
|
67
|
+
:style="{ backgroundColor: slice.color }"
|
|
68
|
+
></div>
|
|
69
|
+
<span class="tw-truncate">
|
|
70
|
+
{{ slice.label }}
|
|
71
|
+
</span>
|
|
72
|
+
</li>
|
|
73
|
+
</ol>
|
|
74
|
+
<!-- paging controls -->
|
|
75
|
+
<div
|
|
76
|
+
v-if="pageCount > 1"
|
|
77
|
+
class="tw-flex tw-justify-center md:tw-justify-start"
|
|
78
|
+
>
|
|
79
|
+
<button
|
|
80
|
+
:disabled="pageIndex === 0"
|
|
81
|
+
class="tw-font-medium tw-p-0.5 disabled:tw-opacity-low"
|
|
82
|
+
@click="prevPage"
|
|
83
|
+
>
|
|
84
|
+
<
|
|
85
|
+
</button>
|
|
86
|
+
{{ pageIndex + 1 }} / {{ pageCount }}
|
|
87
|
+
<button
|
|
88
|
+
:disabled="pageIndex === pageCount - 1"
|
|
89
|
+
class="tw-font-medium tw-p-0.5 disabled:tw-opacity-low"
|
|
90
|
+
@click="nextPage"
|
|
91
|
+
>
|
|
92
|
+
>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</figure>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<script>
|
|
100
|
+
import numeral from 'numeral';
|
|
101
|
+
import { ref, toRefs, computed } from 'vue-demi';
|
|
102
|
+
import Alea from '../utils/Alea';
|
|
103
|
+
import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
|
|
104
|
+
|
|
105
|
+
// convenience function to get point on circumference of a given circle (from https://codepen.io/grieve/pen/xwGMJp)
|
|
106
|
+
function circumPointFromAngle(cx, cy, r, a) {
|
|
107
|
+
return [
|
|
108
|
+
cx + r * Math.cos(a),
|
|
109
|
+
cy + r * Math.sin(a),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default {
|
|
114
|
+
name: 'PieChartFigure',
|
|
115
|
+
components: {
|
|
116
|
+
KvLoadingPlaceholder,
|
|
117
|
+
},
|
|
118
|
+
props: {
|
|
119
|
+
loading: {
|
|
120
|
+
type: Boolean,
|
|
121
|
+
default: true,
|
|
122
|
+
},
|
|
123
|
+
values: {
|
|
124
|
+
type: Array,
|
|
125
|
+
default: () => ([]),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
setup(props) {
|
|
129
|
+
const {
|
|
130
|
+
values,
|
|
131
|
+
} = toRefs(props);
|
|
132
|
+
|
|
133
|
+
const svgSize = ref(32);
|
|
134
|
+
const lineWidth = ref(5);
|
|
135
|
+
const activeSlice = ref(null);
|
|
136
|
+
const pageIndex = ref(0);
|
|
137
|
+
const slicesPerPage = ref(10);
|
|
138
|
+
|
|
139
|
+
const activeSlicePercent = computed(() => {
|
|
140
|
+
return activeSlice.value ? numeral(activeSlice.value.percent).format('0.00%') : '';
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const radius = computed(() => {
|
|
144
|
+
return (svgSize.value / 2) - (lineWidth.value / 2) - 2;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const center = computed(() => {
|
|
148
|
+
return svgSize.value / 2;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const pickColor = (index) => {
|
|
152
|
+
const rng = new Alea('loans', index, 'kiva');
|
|
153
|
+
let color = '#';
|
|
154
|
+
for (let i = 0; i < 3; i += 1) {
|
|
155
|
+
color += Math.floor(rng() * 256).toString(16).padStart(2, '0');
|
|
156
|
+
}
|
|
157
|
+
return color;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const slices = computed(() => {
|
|
161
|
+
const slicesArr = [];
|
|
162
|
+
// center point
|
|
163
|
+
const cX = center.value;
|
|
164
|
+
const cY = cX;
|
|
165
|
+
// radius
|
|
166
|
+
const r = radius.value;
|
|
167
|
+
// starting angle
|
|
168
|
+
let start = -0.25;
|
|
169
|
+
// loop through each value and create a pie slice path
|
|
170
|
+
for (let i = 0; i < values.value.length; i += 1) {
|
|
171
|
+
const value = values.value[i];
|
|
172
|
+
const end = start + value.percent;
|
|
173
|
+
const [startX, startY] = circumPointFromAngle(cX, cY, r, start * Math.PI * 2);
|
|
174
|
+
const [endX, endY] = circumPointFromAngle(cX, cY, r, end * Math.PI * 2);
|
|
175
|
+
const largeArc = value.percent > 0.5 ? 1 : 0;
|
|
176
|
+
// Draw just the outer arc of the slice
|
|
177
|
+
const path = `M ${startX} ${startY} A ${r} ${r} 0 ${largeArc} 1 ${endX} ${endY}`;
|
|
178
|
+
slicesArr.push({
|
|
179
|
+
...value,
|
|
180
|
+
path,
|
|
181
|
+
color: pickColor(i),
|
|
182
|
+
});
|
|
183
|
+
start = end;
|
|
184
|
+
}
|
|
185
|
+
return slicesArr;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const pageCount = computed(() => {
|
|
189
|
+
return Math.ceil(slices.value.length / slicesPerPage.value);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const prevPage = () => {
|
|
193
|
+
if (pageIndex.value > 0) {
|
|
194
|
+
pageIndex.value -= 1;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const nextPage = () => {
|
|
198
|
+
if (pageIndex.value < pageCount.value - 1) {
|
|
199
|
+
pageIndex.value += 1;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
svgSize,
|
|
205
|
+
lineWidth,
|
|
206
|
+
activeSlice,
|
|
207
|
+
pageIndex,
|
|
208
|
+
slicesPerPage,
|
|
209
|
+
activeSlicePercent,
|
|
210
|
+
radius,
|
|
211
|
+
center,
|
|
212
|
+
slices,
|
|
213
|
+
pageCount,
|
|
214
|
+
prevPage,
|
|
215
|
+
nextPage,
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
</script>
|
|
220
|
+
|
|
221
|
+
<style lang="postcss" scoped>
|
|
222
|
+
.pie-chart {
|
|
223
|
+
height: 40rem;
|
|
224
|
+
@screen md {
|
|
225
|
+
height: 20rem;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.pie-placeholder {
|
|
230
|
+
width: 20rem;
|
|
231
|
+
}
|
|
232
|
+
</style>
|