@kiva/kv-components 3.93.0 → 3.94.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 +11 -0
- package/package.json +4 -2
- package/utils/Alea.js +92 -0
- package/utils/touchEvents.js +22 -0
- 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/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,17 @@
|
|
|
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.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)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* charts added to library ([#447](https://github.com/kiva/kv-ui-elements/issues/447)) ([575bd80](https://github.com/kiva/kv-ui-elements/commit/575bd80fb1700a38732a2b8cafbeb5cc5374764b))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [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
18
|
|
|
8
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kiva/kv-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.94.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": "4d6b8d16d7fe0e5b3904b87a82484e28325cd496"
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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>
|
package/vue/KvPopper.vue
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<transition :name="transitionType">
|
|
3
|
+
<div
|
|
4
|
+
v-show="show"
|
|
5
|
+
class="tw-absolute"
|
|
6
|
+
:style="styles"
|
|
7
|
+
:aria-hidden="show ? 'false' : 'true'"
|
|
8
|
+
>
|
|
9
|
+
<slot></slot>
|
|
10
|
+
</div>
|
|
11
|
+
</transition>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
import {
|
|
16
|
+
onBodyTouchstart,
|
|
17
|
+
offBodyTouchstart,
|
|
18
|
+
isTargetElement,
|
|
19
|
+
} from '../utils/touchEvents';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: 'KvPopper',
|
|
23
|
+
props: {
|
|
24
|
+
controller: {
|
|
25
|
+
validator(value) {
|
|
26
|
+
if (typeof value === 'string') return true;
|
|
27
|
+
if (typeof window === 'object'
|
|
28
|
+
&& 'HTMLElement' in window
|
|
29
|
+
&& value instanceof HTMLElement) return true;
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
openDelay: { type: Number, default: 0 },
|
|
35
|
+
closeDelay: { type: Number, default: 200 },
|
|
36
|
+
// must be defined in our globa/transitions.scss
|
|
37
|
+
transitionType: { type: String, default: '' },
|
|
38
|
+
popperPlacement: { type: String, default: 'bottom-start' },
|
|
39
|
+
popperModifiers: { type: Object, default: () => {} },
|
|
40
|
+
},
|
|
41
|
+
data() {
|
|
42
|
+
return {
|
|
43
|
+
popper: null,
|
|
44
|
+
popperPromise: null,
|
|
45
|
+
styles: {},
|
|
46
|
+
show: false,
|
|
47
|
+
timeout: null,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
computed: {
|
|
51
|
+
reference() {
|
|
52
|
+
return typeof this.controller === 'string' ? document.getElementById(this.controller) : this.controller;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
watch: {
|
|
56
|
+
show(showing) {
|
|
57
|
+
if (this.reference) {
|
|
58
|
+
this.reference.setAttribute('aria-expanded', showing ? 'true' : 'false');
|
|
59
|
+
}
|
|
60
|
+
this.$emit(showing ? 'show' : 'hide');
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
mounted() {
|
|
64
|
+
this.reference.tabIndex = 0;
|
|
65
|
+
this.attachEvents();
|
|
66
|
+
},
|
|
67
|
+
updated() {
|
|
68
|
+
if (this.popper) {
|
|
69
|
+
this.popper.scheduleUpdate();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
beforeDestroy() {
|
|
73
|
+
this.removeEvents();
|
|
74
|
+
if (this.popper) {
|
|
75
|
+
this.popper.destroy();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
methods: {
|
|
79
|
+
open() {
|
|
80
|
+
this.initPopper().then(() => {
|
|
81
|
+
this.setTimeout(() => {
|
|
82
|
+
this.show = true;
|
|
83
|
+
this.popper.scheduleUpdate();
|
|
84
|
+
this.attachBodyEvents();
|
|
85
|
+
}, this.openDelay);
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
close() {
|
|
89
|
+
this.setTimeout(() => {
|
|
90
|
+
this.show = false;
|
|
91
|
+
this.removeBodyEvents();
|
|
92
|
+
}, this.closeDelay);
|
|
93
|
+
},
|
|
94
|
+
toggle() {
|
|
95
|
+
if (this.show) {
|
|
96
|
+
this.close();
|
|
97
|
+
} else {
|
|
98
|
+
this.open();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
update() {
|
|
102
|
+
if (this.popper) {
|
|
103
|
+
this.popper.scheduleUpdate();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
initPopper() {
|
|
107
|
+
// skip loading if popper already created
|
|
108
|
+
if (this.popper) return Promise.resolve();
|
|
109
|
+
// skip loading if loading already started
|
|
110
|
+
if (this.popperPromise) return this.popperPromise;
|
|
111
|
+
// import and init Popper.js
|
|
112
|
+
this.popperPromise = import('popper.js').then(({ default: Popper }) => {
|
|
113
|
+
this.popper = new Popper(this.reference, this.$el, {
|
|
114
|
+
placement: this.popperPlacement,
|
|
115
|
+
modifiers: {
|
|
116
|
+
applyStyle: (data) => {
|
|
117
|
+
this.styles = data.styles;
|
|
118
|
+
this.setAttributes(data.attributes);
|
|
119
|
+
},
|
|
120
|
+
preventOverflow: {
|
|
121
|
+
padding: 0,
|
|
122
|
+
},
|
|
123
|
+
...this.popperModifiers,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return this.popperPromise;
|
|
128
|
+
},
|
|
129
|
+
bodyTouchHandler(e) {
|
|
130
|
+
if (!isTargetElement(e, [this.reference, this.$el])) {
|
|
131
|
+
this.show = false;
|
|
132
|
+
this.removeBodyEvents();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
referenceTapHandler(e) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
this.toggle();
|
|
138
|
+
},
|
|
139
|
+
attachEvents() {
|
|
140
|
+
this.reference.addEventListener('mouseover', this.open);
|
|
141
|
+
this.reference.addEventListener('focus', this.open);
|
|
142
|
+
this.reference.addEventListener('mouseout', this.close);
|
|
143
|
+
this.reference.addEventListener('blur', this.close);
|
|
144
|
+
this.$el.addEventListener('mouseover', this.open);
|
|
145
|
+
this.$el.addEventListener('mouseout', this.close);
|
|
146
|
+
this.reference.addEventListener('touchstart', this.referenceTapHandler);
|
|
147
|
+
},
|
|
148
|
+
attachBodyEvents() {
|
|
149
|
+
onBodyTouchstart(this.bodyTouchHandler);
|
|
150
|
+
},
|
|
151
|
+
removeEvents() {
|
|
152
|
+
this.removeBodyEvents();
|
|
153
|
+
this.reference.removeEventListener('touchstart', this.referenceTapHandler);
|
|
154
|
+
this.reference.removeEventListener('mouseover', this.open);
|
|
155
|
+
this.reference.removeEventListener('mouseout', this.close);
|
|
156
|
+
this.$el.removeEventListener('mouseover', this.open);
|
|
157
|
+
this.$el.removeEventListener('mouseout', this.close);
|
|
158
|
+
},
|
|
159
|
+
removeBodyEvents() {
|
|
160
|
+
offBodyTouchstart(this.bodyTouchHandler);
|
|
161
|
+
},
|
|
162
|
+
setAttributes(attrs) {
|
|
163
|
+
Object.keys(attrs).forEach((attr) => {
|
|
164
|
+
const value = attrs[attr];
|
|
165
|
+
if (value === false) {
|
|
166
|
+
this.$el.removeAttribute(attr);
|
|
167
|
+
} else {
|
|
168
|
+
this.$el.setAttribute(attr, value);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
setTimeout(fn, delay) {
|
|
173
|
+
window.clearTimeout(this.timeout);
|
|
174
|
+
this.timeout = window.setTimeout(fn, delay);
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
</script>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<kv-theme-provider
|
|
3
|
+
:theme="themeStyle"
|
|
4
|
+
class="kv-tailwind"
|
|
5
|
+
>
|
|
6
|
+
<kv-popper
|
|
7
|
+
:controller="controller"
|
|
8
|
+
:popper-modifiers="popperModifiers"
|
|
9
|
+
popper-placement="top"
|
|
10
|
+
transition-type="kvfastfade"
|
|
11
|
+
class="tooltip-pane tw-absolute tw-bg-primary tw-rounded tw-z-popover"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="tw-p-2.5"
|
|
15
|
+
style="max-width: 250px;"
|
|
16
|
+
>
|
|
17
|
+
<div
|
|
18
|
+
v-if="$slots.title"
|
|
19
|
+
class="tw-text-primary tw-font-medium tw-mb-1.5"
|
|
20
|
+
>
|
|
21
|
+
<slot name="title"></slot>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="tw-text-primary">
|
|
24
|
+
<slot></slot>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div
|
|
28
|
+
class="tooltip-arrow tw-absolute tw-w-0 tw-h-0 tw-border-solid"
|
|
29
|
+
x-arrow=""
|
|
30
|
+
></div>
|
|
31
|
+
</kv-popper>
|
|
32
|
+
</kv-theme-provider>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import { ref, toRefs, computed } from 'vue-demi';
|
|
37
|
+
import {
|
|
38
|
+
darkTheme,
|
|
39
|
+
mintTheme,
|
|
40
|
+
} from '@kiva/kv-tokens/configs/kivaColors.cjs';
|
|
41
|
+
import KvPopper from './KvPopper.vue';
|
|
42
|
+
import KvThemeProvider from './KvThemeProvider.vue';
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
name: 'KvTooltip',
|
|
46
|
+
components: {
|
|
47
|
+
KvPopper,
|
|
48
|
+
KvThemeProvider,
|
|
49
|
+
},
|
|
50
|
+
// TODO: Add prop for tooltip placement, Currently defaults to 'top' but will flip to bottom when constrained
|
|
51
|
+
props: {
|
|
52
|
+
controller: {
|
|
53
|
+
validator(value) {
|
|
54
|
+
if (typeof value === 'string') return true;
|
|
55
|
+
if (typeof window !== 'undefined'
|
|
56
|
+
&& 'HTMLElement' in window
|
|
57
|
+
&& value instanceof HTMLElement) return true;
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
required: true,
|
|
61
|
+
},
|
|
62
|
+
theme: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: 'default',
|
|
65
|
+
validator(value) {
|
|
66
|
+
// The value must match one of these strings
|
|
67
|
+
return ['default', 'mint', 'dark'].indexOf(value) !== -1;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
setup(props) {
|
|
72
|
+
const {
|
|
73
|
+
theme,
|
|
74
|
+
} = toRefs(props);
|
|
75
|
+
|
|
76
|
+
const popperModifiers = ref({
|
|
77
|
+
preventOverflow: {
|
|
78
|
+
padding: 10,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const themeStyle = computed(() => {
|
|
83
|
+
const themeMapper = {
|
|
84
|
+
mint: mintTheme,
|
|
85
|
+
dark: darkTheme,
|
|
86
|
+
};
|
|
87
|
+
return themeMapper[theme.value];
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
darkTheme,
|
|
92
|
+
mintTheme,
|
|
93
|
+
popperModifiers,
|
|
94
|
+
themeStyle,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<style lang="postcss" scoped>
|
|
101
|
+
.tooltip-pane,
|
|
102
|
+
.tooltip-arrow {
|
|
103
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.tooltip-arrow {
|
|
107
|
+
@apply tw-m-1;
|
|
108
|
+
@apply tw-border-white;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Top Tooltip Arrow appears on Bottom */
|
|
112
|
+
.tooltip-pane[x-placement^="top"] {
|
|
113
|
+
@apply tw-mb-1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.tooltip-pane[x-placement^="top"] .tooltip-arrow {
|
|
117
|
+
border-width: 8px 8px 0 8px;
|
|
118
|
+
border-left-color: transparent;
|
|
119
|
+
border-right-color: transparent;
|
|
120
|
+
border-bottom-color: transparent;
|
|
121
|
+
left: calc(50% - 8px);
|
|
122
|
+
@apply -tw-bottom-1 tw-mt-0 tw-mb-0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Bottom Tooltip Arrow appears on Top */
|
|
126
|
+
.tooltip-pane[x-placement^="bottom"] {
|
|
127
|
+
@apply tw-mt-1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.tooltip-pane[x-placement^="bottom"] .tooltip-arrow {
|
|
131
|
+
border-width: 0 8px 8px 8px;
|
|
132
|
+
border-left-color: transparent;
|
|
133
|
+
border-right-color: transparent;
|
|
134
|
+
border-top-color: transparent;
|
|
135
|
+
left: calc(50% - 8px);
|
|
136
|
+
@apply -tw-top-1 tw-mb-0 tw-mt-0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* TODO: TWEAK Inner Arrow Styles for Left + Right Orientations */
|
|
140
|
+
|
|
141
|
+
/* Right Side Tooltip, Arrow appears on Left */
|
|
142
|
+
.tooltip-pane[x-placement^="right"] {
|
|
143
|
+
@apply tw-ml-1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.tooltip-pane[x-placement^="right"] .tooltip-arrow {
|
|
147
|
+
border-width: 8px 8px 8px 0;
|
|
148
|
+
border-left-color: transparent;
|
|
149
|
+
border-top-color: transparent;
|
|
150
|
+
border-bottom-color: transparent;
|
|
151
|
+
top: calc(50% - 8px);
|
|
152
|
+
@apply -tw-left-1 tw-ml-0 tw-mr-0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Left Side Tooltip, Arrow appears on Right */
|
|
156
|
+
.tooltip-pane[x-placement^="left"] {
|
|
157
|
+
@apply tw-mr-1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.tooltip-pane[x-placement^="left"] .tooltip-arrow {
|
|
161
|
+
border-width: 8px 0 8px 8px;
|
|
162
|
+
border-top-color: transparent;
|
|
163
|
+
border-right-color: transparent;
|
|
164
|
+
border-bottom-color: transparent;
|
|
165
|
+
top: calc(50% - 8px);
|
|
166
|
+
@apply -tw-right-1 tw-ml-0 tw-mr-0;
|
|
167
|
+
}
|
|
168
|
+
</style>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<figure class="treemap-figure tw-relative">
|
|
3
|
+
<!-- treemap -->
|
|
4
|
+
<div
|
|
5
|
+
v-for="(block, index) in blocks"
|
|
6
|
+
:key="`block-${index}`"
|
|
7
|
+
class="tw-absolute"
|
|
8
|
+
:style="blockPositionStyles(block)"
|
|
9
|
+
>
|
|
10
|
+
<kv-loading-placeholder
|
|
11
|
+
v-if="loading"
|
|
12
|
+
style="width: calc(100% - 0.25rem); height: calc(100% - 0.25rem);"
|
|
13
|
+
/>
|
|
14
|
+
<p
|
|
15
|
+
v-if="!loading"
|
|
16
|
+
class="tw-rounded-sm tw-box-border tw-overflow-hidden tw-text-small"
|
|
17
|
+
:class="colorClasses(index)"
|
|
18
|
+
style="width: calc(100% - 0.25rem); height: calc(100% - 0.25rem);"
|
|
19
|
+
@mouseenter="setHoverBlock(block)"
|
|
20
|
+
>
|
|
21
|
+
<span class="tw-block tw-px-1 tw-py-0.5 tw-truncate">
|
|
22
|
+
{{ block.data.label }} {{ block.data.percent }}
|
|
23
|
+
</span>
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
<!-- tooltip -->
|
|
27
|
+
<div
|
|
28
|
+
ref="tooltipController"
|
|
29
|
+
class="tw-absolute"
|
|
30
|
+
:style="blockPositionStyles(activeBlock)"
|
|
31
|
+
></div>
|
|
32
|
+
<kv-tooltip
|
|
33
|
+
v-if="!loading && tooltipControllerElement"
|
|
34
|
+
:controller="tooltipControllerElement"
|
|
35
|
+
>
|
|
36
|
+
<template #title>
|
|
37
|
+
{{ activeBlock.data.label }}
|
|
38
|
+
</template>
|
|
39
|
+
{{ activeBlock.data.value }} ({{ activeBlock.data.percent }})
|
|
40
|
+
</kv-tooltip>
|
|
41
|
+
</figure>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script>
|
|
45
|
+
import numeral from 'numeral';
|
|
46
|
+
import { getTreemap } from 'treemap-squarify';
|
|
47
|
+
import kvTokensPrimitives from '@kiva/kv-tokens/primitives.json';
|
|
48
|
+
import { throttle } from '../utils/throttle';
|
|
49
|
+
import KvTooltip from './KvTooltip.vue';
|
|
50
|
+
import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
|
|
51
|
+
|
|
52
|
+
const { breakpoints } = kvTokensPrimitives;
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
name: 'TreeMapFigure',
|
|
56
|
+
components: {
|
|
57
|
+
KvLoadingPlaceholder,
|
|
58
|
+
KvTooltip,
|
|
59
|
+
},
|
|
60
|
+
props: {
|
|
61
|
+
loading: {
|
|
62
|
+
type: Boolean,
|
|
63
|
+
default: true,
|
|
64
|
+
},
|
|
65
|
+
values: {
|
|
66
|
+
type: Array,
|
|
67
|
+
default: () => [],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
screenWidth: 0,
|
|
73
|
+
activeBlock: { data: {} },
|
|
74
|
+
tooltipControllerElement: null,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
computed: {
|
|
78
|
+
activeBreakpoints() {
|
|
79
|
+
return Object.keys(breakpoints)
|
|
80
|
+
.filter((name) => this.screenWidth >= breakpoints[name]);
|
|
81
|
+
},
|
|
82
|
+
// Used to determine if blocks should be displayed in landscape or portrait orientation
|
|
83
|
+
aboveMedium() {
|
|
84
|
+
return this.activeBreakpoints.includes('md');
|
|
85
|
+
},
|
|
86
|
+
// Calculate the blocks of the treemap, but don't apply any formatting
|
|
87
|
+
rawBlocks() {
|
|
88
|
+
// Use static blocks when no data is available (either loading or no loans)
|
|
89
|
+
if (!this.values?.length) {
|
|
90
|
+
return this.loading ? [
|
|
91
|
+
{
|
|
92
|
+
x: 0, y: 0, width: 60, height: 40,
|
|
93
|
+
},
|
|
94
|
+
{ x: 60, y: 0, height: 40 },
|
|
95
|
+
{ x: 0, y: 40, width: 30 },
|
|
96
|
+
{ x: 30, y: 40 },
|
|
97
|
+
] : [
|
|
98
|
+
{ x: 0, y: 0, data: { label: 'No loans yet' } },
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Calculate treemap blocks using canvas size 100x100 to easily translate to percentages
|
|
103
|
+
const blocks = getTreemap({
|
|
104
|
+
data: this.values,
|
|
105
|
+
width: 100,
|
|
106
|
+
height: 100,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Blocks smaller than 8% in either dimension will not display well,
|
|
110
|
+
// so combine them into one large 'Other' block
|
|
111
|
+
// If we only have 1 small block though, don't make the 'Other' block
|
|
112
|
+
const tooSmall = blocks.filter((block) => block.width <= 8 || block.height <= 8);
|
|
113
|
+
if (tooSmall.length === 1) return blocks;
|
|
114
|
+
|
|
115
|
+
const bigBlocks = blocks.filter((block) => block.width > 8 && block.height > 8);
|
|
116
|
+
return [...bigBlocks, this.reduceBlocks(tooSmall)];
|
|
117
|
+
},
|
|
118
|
+
// This is the array that is iterated over in the template
|
|
119
|
+
blocks() {
|
|
120
|
+
return this.rawBlocks.map((block) => {
|
|
121
|
+
const {
|
|
122
|
+
data, x, y, width, height,
|
|
123
|
+
} = block ?? {};
|
|
124
|
+
const { percent } = data ?? {};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
data: {
|
|
128
|
+
...data,
|
|
129
|
+
percent: numeral(percent).format('0[.]0%'),
|
|
130
|
+
},
|
|
131
|
+
// flip x/y when going between small (portrait) and medium (landscape) screens
|
|
132
|
+
x: this.aboveMedium ? x : y,
|
|
133
|
+
y: this.aboveMedium ? y : x,
|
|
134
|
+
width: this.aboveMedium ? width : height,
|
|
135
|
+
height: this.aboveMedium ? height : width,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
mounted() {
|
|
141
|
+
this.screenWidth = window.innerWidth;
|
|
142
|
+
window.addEventListener('resize', throttle(() => {
|
|
143
|
+
this.screenWidth = window.innerWidth;
|
|
144
|
+
}, 200));
|
|
145
|
+
|
|
146
|
+
// Used by KvTooltip/KvPopper to position the tooltip
|
|
147
|
+
if (this.$refs?.tooltipController) {
|
|
148
|
+
this.tooltipControllerElement = this.$refs.tooltipController;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
methods: {
|
|
152
|
+
// Used by the p elements in the main v-for loop to determine background and text color
|
|
153
|
+
colorClasses(index) {
|
|
154
|
+
const classes = [];
|
|
155
|
+
const total = this.blocks?.length ?? 1;
|
|
156
|
+
|
|
157
|
+
// background
|
|
158
|
+
const size = (10 - Math.round((index / total) * 9)) * 100;
|
|
159
|
+
classes.push(size === 600 ? 'tw-bg-brand' : `tw-bg-brand-${size}`);
|
|
160
|
+
|
|
161
|
+
// text
|
|
162
|
+
classes.push(size > 700 ? 'tw-text-white' : 'tw-text-black');
|
|
163
|
+
|
|
164
|
+
return classes;
|
|
165
|
+
},
|
|
166
|
+
// Used by the divs in the main v-for for block positions and the tooltip controller div to match positions
|
|
167
|
+
// with the current active block
|
|
168
|
+
blockPositionStyles(block) {
|
|
169
|
+
return {
|
|
170
|
+
left: `${block.x}%`,
|
|
171
|
+
top: `${block.y}%`,
|
|
172
|
+
width: block.width ? `${block.width}%` : null,
|
|
173
|
+
height: block.height ? `${block.height}%` : null,
|
|
174
|
+
right: block.width ? null : '0',
|
|
175
|
+
bottom: block.height ? null : '0',
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
// Combine all small blocks into a single 'Other' block that will be at the bottom right of the figure.
|
|
179
|
+
reduceBlocks(blocks) {
|
|
180
|
+
return blocks?.reduce((other, block) => {
|
|
181
|
+
/* eslint-disable no-param-reassign */
|
|
182
|
+
// Use the smallest x for the overall x
|
|
183
|
+
if (block.x < other.x) {
|
|
184
|
+
other.x = block.x;
|
|
185
|
+
}
|
|
186
|
+
// Use the smallest y for the overall y
|
|
187
|
+
if (block.y < other.y) {
|
|
188
|
+
other.y = block.y;
|
|
189
|
+
}
|
|
190
|
+
// Sum up all values and percents
|
|
191
|
+
other.data.value += block.data.value;
|
|
192
|
+
other.data.percent += block.data.percent;
|
|
193
|
+
return other;
|
|
194
|
+
/* eslint-ensable no-param-reassign */
|
|
195
|
+
}, {
|
|
196
|
+
x: 100,
|
|
197
|
+
y: 100,
|
|
198
|
+
width: 0,
|
|
199
|
+
height: 0,
|
|
200
|
+
data: {
|
|
201
|
+
label: 'Other',
|
|
202
|
+
value: 0,
|
|
203
|
+
percent: 0,
|
|
204
|
+
},
|
|
205
|
+
}) ?? [];
|
|
206
|
+
},
|
|
207
|
+
// Sets the block that will be used to position the tooltip and change the info displayed in the tooltip
|
|
208
|
+
setHoverBlock(block) {
|
|
209
|
+
this.activeBlock = block;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
</script>
|
|
214
|
+
|
|
215
|
+
<style lang="postcss" scoped>
|
|
216
|
+
.treemap-figure {
|
|
217
|
+
height: 30rem;
|
|
218
|
+
|
|
219
|
+
/* account for every block having a 0.25rem right margin */
|
|
220
|
+
width: calc(100% + 0.25rem);
|
|
221
|
+
margin-right: -0.25rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@screen md {
|
|
225
|
+
.treemap-figure {
|
|
226
|
+
height: 20rem;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import KvPieChart from '../KvPieChart.vue';
|
|
2
|
+
|
|
3
|
+
const genderValues = [{ label: 'Female', value: 1243, percent: 0.6467221644120708 }, { label: 'Male', value: 676, percent: 0.3517169614984391 }, { label: 'Unspecified', value: 2, percent: 0.001040582726326743 }, { label: 'Nonbinary', value: 1, percent: 0.0005202913631633715 }];
|
|
4
|
+
const sectorValues = [{ label: 'Food', value: 575, percent: 0.2991675338189386 }, { label: 'Retail', value: 377, percent: 0.19614984391259105 }, { label: 'Agriculture', value: 285, percent: 0.14828303850156088 }, { label: 'Services', value: 211, percent: 0.10978147762747138 }, { label: 'Clothing', value: 183, percent: 0.09521331945889698 }, { label: 'Arts', value: 65, percent: 0.033818938605619145 }, { label: 'Housing', value: 65, percent: 0.033818938605619145 }, { label: 'Education', value: 36, percent: 0.018730489073881373 }, { label: 'Construction', value: 28, percent: 0.014568158168574402 }, { label: 'Health', value: 27, percent: 0.01404786680541103 }, { label: 'Transportation', value: 23, percent: 0.011966701352757543 }, { label: 'Personal Use', value: 19, percent: 0.009885535900104058 }, { label: 'Manufacturing', value: 13, percent: 0.006763787721123829 }, { label: 'Entertainment', value: 10, percent: 0.005202913631633715 }, { label: 'Wholesale', value: 5, percent: 0.0026014568158168575 }];
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Charts/KvPieChart',
|
|
8
|
+
component: KvPieChart,
|
|
9
|
+
args: {
|
|
10
|
+
loading: false,
|
|
11
|
+
values: genderValues,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Template = (args, {
|
|
16
|
+
argTypes,
|
|
17
|
+
}) => ({
|
|
18
|
+
props: Object.keys(argTypes),
|
|
19
|
+
components: {
|
|
20
|
+
KvPieChart,
|
|
21
|
+
},
|
|
22
|
+
template: `
|
|
23
|
+
<div>
|
|
24
|
+
<kv-pie-chart
|
|
25
|
+
:values="values"
|
|
26
|
+
:loading="loading"
|
|
27
|
+
/>
|
|
28
|
+
</div>`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const Default = Template.bind({});
|
|
32
|
+
|
|
33
|
+
export const Loading = Template.bind({});
|
|
34
|
+
Loading.args = {
|
|
35
|
+
loading: true,
|
|
36
|
+
values: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ManyValues = Template.bind({});
|
|
40
|
+
ManyValues.args = {
|
|
41
|
+
values: sectorValues,
|
|
42
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import KvTooltip from '../KvTooltip.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'KvTooltip',
|
|
5
|
+
component: KvTooltip,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Default = () => ({
|
|
9
|
+
components: {
|
|
10
|
+
KvTooltip,
|
|
11
|
+
},
|
|
12
|
+
template: `
|
|
13
|
+
<div>
|
|
14
|
+
<button id="my-cool-btn">Hover of Focus Me!</button>
|
|
15
|
+
<kv-tooltip controller="my-cool-btn">
|
|
16
|
+
<template #title>
|
|
17
|
+
What is an Experimental Field Partner?
|
|
18
|
+
</template>
|
|
19
|
+
If a Field Partner is labeled as Experimental, this means that Kiva has
|
|
20
|
+
required only a comparatively light level of due diligence and
|
|
21
|
+
monitoring, in exchange for only allowing this Field Partner access to a
|
|
22
|
+
small amount of funding through Kiva at any given time.
|
|
23
|
+
</kv-tooltip>
|
|
24
|
+
</div>
|
|
25
|
+
`,
|
|
26
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import KvTreeMapChart from '../KvTreeMapChart.vue';
|
|
2
|
+
|
|
3
|
+
const genderValues = [{ label: 'Female', value: 1243, percent: 0.6467221644120708 }, { label: 'Male', value: 676, percent: 0.3517169614984391 }, { label: 'Unspecified', value: 2, percent: 0.001040582726326743 }, { label: 'Nonbinary', value: 1, percent: 0.0005202913631633715 }];
|
|
4
|
+
const sectorValues = [{ label: 'Food', value: 575, percent: 0.2991675338189386 }, { label: 'Retail', value: 377, percent: 0.19614984391259105 }, { label: 'Agriculture', value: 285, percent: 0.14828303850156088 }, { label: 'Services', value: 211, percent: 0.10978147762747138 }, { label: 'Clothing', value: 183, percent: 0.09521331945889698 }, { label: 'Arts', value: 65, percent: 0.033818938605619145 }, { label: 'Housing', value: 65, percent: 0.033818938605619145 }, { label: 'Education', value: 36, percent: 0.018730489073881373 }, { label: 'Construction', value: 28, percent: 0.014568158168574402 }, { label: 'Health', value: 27, percent: 0.01404786680541103 }, { label: 'Transportation', value: 23, percent: 0.011966701352757543 }, { label: 'Personal Use', value: 19, percent: 0.009885535900104058 }, { label: 'Manufacturing', value: 13, percent: 0.006763787721123829 }, { label: 'Entertainment', value: 10, percent: 0.005202913631633715 }, { label: 'Wholesale', value: 5, percent: 0.0026014568158168575 }];
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Charts/KvTreeMapChart',
|
|
8
|
+
component: KvTreeMapChart,
|
|
9
|
+
args: {
|
|
10
|
+
loading: false,
|
|
11
|
+
values: genderValues,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Template = (args, {
|
|
16
|
+
argTypes,
|
|
17
|
+
}) => ({
|
|
18
|
+
props: Object.keys(argTypes),
|
|
19
|
+
components: {
|
|
20
|
+
KvTreeMapChart,
|
|
21
|
+
},
|
|
22
|
+
template: `
|
|
23
|
+
<div>
|
|
24
|
+
<kv-tree-map-chart
|
|
25
|
+
:values="values"
|
|
26
|
+
:loading="loading"
|
|
27
|
+
/>
|
|
28
|
+
</div>`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const Default = Template.bind({});
|
|
32
|
+
|
|
33
|
+
export const Loading = Template.bind({});
|
|
34
|
+
Loading.args = {
|
|
35
|
+
loading: true,
|
|
36
|
+
values: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ManyValues = Template.bind({});
|
|
40
|
+
ManyValues.args = {
|
|
41
|
+
values: sectorValues,
|
|
42
|
+
};
|