@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/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>
|
|
@@ -457,3 +457,24 @@ export const ContributorsAndAmount = story({
|
|
|
457
457
|
photoPath,
|
|
458
458
|
showContributors: true,
|
|
459
459
|
});
|
|
460
|
+
|
|
461
|
+
export const USLoan = story({
|
|
462
|
+
loanId: loan.id,
|
|
463
|
+
loan: {
|
|
464
|
+
...loan,
|
|
465
|
+
geocode: {
|
|
466
|
+
city: 'Kittanning',
|
|
467
|
+
state: 'PA',
|
|
468
|
+
country: {
|
|
469
|
+
isoCode: 'US',
|
|
470
|
+
name: 'United States',
|
|
471
|
+
region: 'North America',
|
|
472
|
+
__typename: 'Country',
|
|
473
|
+
},
|
|
474
|
+
__typename: 'Geocode',
|
|
475
|
+
},
|
|
476
|
+
distributionModel: 'direct',
|
|
477
|
+
},
|
|
478
|
+
kvTrackFunction,
|
|
479
|
+
photoPath,
|
|
480
|
+
});
|
|
@@ -84,14 +84,7 @@ const photoPath = 'https://www-kiva-org.freetls.fastly.net/img/';
|
|
|
84
84
|
|
|
85
85
|
export const Default = story({
|
|
86
86
|
loanId: loan.id,
|
|
87
|
-
loan
|
|
88
|
-
...loan,
|
|
89
|
-
loanFundraisingInfo: {
|
|
90
|
-
fundedAmount: '200.00',
|
|
91
|
-
isExpiringSoon: false,
|
|
92
|
-
reservedAmount: '0.00',
|
|
93
|
-
},
|
|
94
|
-
},
|
|
87
|
+
loan,
|
|
95
88
|
kvTrackFunction,
|
|
96
89
|
photoPath,
|
|
97
90
|
});
|
|
@@ -120,13 +113,87 @@ export const Matched = story({
|
|
|
120
113
|
...loan,
|
|
121
114
|
matchingText: 'Ebay',
|
|
122
115
|
matchRatio: 1,
|
|
116
|
+
},
|
|
117
|
+
kvTrackFunction,
|
|
118
|
+
photoPath,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const LseLoan = story({
|
|
122
|
+
loanId: loan.id,
|
|
123
|
+
loan: {
|
|
124
|
+
...loan,
|
|
125
|
+
partnerName: 'N/A, direct to Novulis',
|
|
126
|
+
},
|
|
127
|
+
kvTrackFunction,
|
|
128
|
+
photoPath,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const AlmostFunded = story({
|
|
132
|
+
loanId: loan.id,
|
|
133
|
+
loan: {
|
|
134
|
+
...loan,
|
|
135
|
+
loanAmount: '100.00',
|
|
136
|
+
unreservedAmount: '10.00',
|
|
137
|
+
fundraisingPercent: 0.9,
|
|
123
138
|
loanFundraisingInfo: {
|
|
124
|
-
fundedAmount: '
|
|
125
|
-
isExpiringSoon: false,
|
|
139
|
+
fundedAmount: '90.00',
|
|
126
140
|
reservedAmount: '0.00',
|
|
127
141
|
},
|
|
128
142
|
},
|
|
129
143
|
kvTrackFunction,
|
|
130
144
|
photoPath,
|
|
131
|
-
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const tomorrow = new Date();
|
|
148
|
+
tomorrow.setDate(new Date().getDate() + 1);
|
|
149
|
+
|
|
150
|
+
export const ExpiringSoon = story({
|
|
151
|
+
loanId: loan.id,
|
|
152
|
+
loan: {
|
|
153
|
+
...loan,
|
|
154
|
+
plannedExpirationDate: tomorrow.toISOString(),
|
|
155
|
+
},
|
|
156
|
+
kvTrackFunction,
|
|
157
|
+
photoPath,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const Funded = story({
|
|
161
|
+
loanId: loan.id,
|
|
162
|
+
loan: {
|
|
163
|
+
...loan,
|
|
164
|
+
unreservedAmount: '0.00',
|
|
165
|
+
},
|
|
166
|
+
kvTrackFunction,
|
|
167
|
+
photoPath,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export const LongName = story({
|
|
171
|
+
loanId: loan.id,
|
|
172
|
+
loan: {
|
|
173
|
+
...loan,
|
|
174
|
+
name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
|
175
|
+
},
|
|
176
|
+
kvTrackFunction,
|
|
177
|
+
photoPath,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const USLoan = story({
|
|
181
|
+
loanId: loan.id,
|
|
182
|
+
loan: {
|
|
183
|
+
...loan,
|
|
184
|
+
geocode: {
|
|
185
|
+
city: 'Kittanning',
|
|
186
|
+
state: 'PA',
|
|
187
|
+
country: {
|
|
188
|
+
isoCode: 'US',
|
|
189
|
+
name: 'United States',
|
|
190
|
+
region: 'North America',
|
|
191
|
+
__typename: 'Country',
|
|
192
|
+
},
|
|
193
|
+
__typename: 'Geocode',
|
|
194
|
+
},
|
|
195
|
+
distributionModel: 'direct',
|
|
196
|
+
},
|
|
197
|
+
kvTrackFunction,
|
|
198
|
+
photoPath,
|
|
132
199
|
});
|