@eturnity/eturnity_reusable_components 8.13.3-EPDM-14458.0 → 8.13.3-EPDM-14657.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.
@@ -0,0 +1,376 @@
1
+ <template>
2
+ <Container :class="`barchart-${chartId}`" :width="width">
3
+ <ChartControlsWrapper
4
+ v-if="isChartControlsShown('top')"
5
+ :position="chartControlsPosition"
6
+ >
7
+ <ChartControls
8
+ :is-legend-shown="isLegendShown"
9
+ :legends-item-per-row="legendsItemPerRow"
10
+ :position="chartControlsPosition"
11
+ :selected-split-button="selectedSplitButton"
12
+ :series="series"
13
+ :split-button-options="splitButtonOptions"
14
+ :stacked-colors="getStackedColors"
15
+ :y-axis-width="yAxisWidth"
16
+ @select-split-button="handleSelectSplitButton"
17
+ />
18
+ </ChartControlsWrapper>
19
+ <GraphSection :height="height" :width="width">
20
+ <YAxis :width="yAxisWidth" :height="height">
21
+ <YAxisTitleWrapper v-if="yAxisTitle" :height="yAxisHeight">
22
+ {{ yAxisTitle }}
23
+ </YAxisTitleWrapper>
24
+ <YAxisRow
25
+ v-for="label in yAxisLabels"
26
+ :key="label"
27
+ :percentage="
28
+ Number(
29
+ ((label / yAxisLabels[yAxisLabels.length - 1]) * 100).toFixed(4)
30
+ )
31
+ "
32
+ >
33
+ <YAxisLabel>{{ label }}</YAxisLabel>
34
+ <YAxisLine :y-axis-width="yAxisWidth" />
35
+ </YAxisRow>
36
+ </YAxis>
37
+
38
+ <ScrollContainer
39
+ :class="`chart-scroll-container-${chartId}`"
40
+ :is-scrollable="isScrollable"
41
+ :height="height"
42
+ @scroll="handleChartScroll"
43
+ >
44
+ <ChartContent
45
+ ref="chartContent"
46
+ :bar-width="barWidth"
47
+ :height="height"
48
+ :is-scrollable="isScrollable"
49
+ :total-bars="normalizedData.length"
50
+ >
51
+ <SelectionBox
52
+ v-if="selectionSize && isSelectionEnabled"
53
+ :bar-width="barWidth"
54
+ :bars-to-show="selectionSize"
55
+ :container-width="chartContentWidth"
56
+ :is-scrollable="isScrollable"
57
+ :total-bars="normalizedData.length"
58
+ @drag-end="handleSelectionDragEnd"
59
+ @update-selection="updateSelectedBars"
60
+ />
61
+ <BarsContainer>
62
+ <BarGroup
63
+ v-for="(item, index) in normalizedData"
64
+ :bar-width="barWidth"
65
+ class="bar-group"
66
+ :is-scrollable="isScrollable"
67
+ :key="index"
68
+ >
69
+ <BarWrapper>
70
+ <BarSegment
71
+ v-for="(segment, segIndex) in item.segments"
72
+ class="bar-segment"
73
+ :gradient-from="getSegmentGradient(index, segment).from"
74
+ :gradient-to="getSegmentGradient(index, segment).to"
75
+ :height="`${segment.percentage}%`"
76
+ :key="segIndex"
77
+ :z-index="item.segments.length - segIndex"
78
+ @mouseenter="showTooltip(item, $event, series)"
79
+ @mouseleave="hideTooltip"
80
+ />
81
+ </BarWrapper>
82
+ <XAxisLine />
83
+ <XAxisLabelHighlight v-if="isSelectionBoundary(index)">
84
+ {{ item.label }}
85
+ </XAxisLabelHighlight>
86
+ <XAxisLabel v-else>{{ item.label }}</XAxisLabel>
87
+ </BarGroup>
88
+ </BarsContainer>
89
+ </ChartContent>
90
+ </ScrollContainer>
91
+ <Tooltip
92
+ v-if="showTooltipContent"
93
+ :left="tooltipStyle.left"
94
+ :top="tooltipStyle.top"
95
+ >
96
+ <slot :item="tooltipData" name="tooltip" />
97
+ <TooltipTextWrapper v-if="!slots.tooltip && tooltipData">
98
+ <template v-if="!series.length">
99
+ <TooltipText font-weight="500">{{ tooltipData.label }}</TooltipText>
100
+ <TooltipText>
101
+ {{ handleValueFormatter(tooltipData.segments[0].value) }}
102
+ </TooltipText>
103
+ </template>
104
+
105
+ <template v-else>
106
+ <TooltipRow>
107
+ <TooltipText font-weight="500">{{
108
+ tooltipData.label
109
+ }}</TooltipText>
110
+ <TooltipText>
111
+ {{
112
+ handleValueFormatter(
113
+ getTotalSegmentValue(tooltipData.segments)
114
+ )
115
+ }}
116
+ </TooltipText>
117
+ </TooltipRow>
118
+ <template
119
+ v-for="(segment, index) in [...tooltipData.segments].reverse()"
120
+ :key="index"
121
+ >
122
+ <TooltipRow>
123
+ <TooltipGradientBox
124
+ :gradient-from="segment.gradientFrom"
125
+ :gradient-to="segment.gradientTo"
126
+ />
127
+ <TooltipText>
128
+ {{ handleValueFormatter(segment.value) }}
129
+ </TooltipText>
130
+ </TooltipRow>
131
+ </template>
132
+ </template>
133
+ </TooltipTextWrapper>
134
+ </Tooltip>
135
+ </GraphSection>
136
+ <ChartControlsWrapper
137
+ v-if="isChartControlsShown('bottom')"
138
+ :position="chartControlsPosition"
139
+ >
140
+ <ChartControls
141
+ :is-legend-shown="isLegendShown"
142
+ :legends-item-per-row="legendsItemPerRow"
143
+ :position="chartControlsPosition"
144
+ :selected-split-button="selectedSplitButton"
145
+ :series="series"
146
+ :split-button-options="splitButtonOptions"
147
+ :stacked-colors="getStackedColors"
148
+ :y-axis-width="yAxisWidth"
149
+ @select-split-button="handleSelectSplitButton"
150
+ />
151
+ </ChartControlsWrapper>
152
+ <BottomFields
153
+ v-if="isBottomFieldsShown"
154
+ :bar-width="barWidth"
155
+ :chart-id="chartId"
156
+ :data="data"
157
+ :field-mode="fieldMode"
158
+ :is-chart-controls-shown-in-bottom="isChartControlsShown('bottom')"
159
+ :is-scrollable="isScrollable"
160
+ :series="series"
161
+ :y-axis-width="yAxisWidth"
162
+ @input-blur="handleInputBlur"
163
+ @input-blur-all="handleInputBlurAll"
164
+ @input-focus="showTooltipFromInput"
165
+ @sync-scroll="handleBottomFieldsScroll"
166
+ />
167
+ </Container>
168
+ </template>
169
+
170
+ <script setup>
171
+ import { useSlots, computed } from 'vue'
172
+
173
+ import ChartControls from './ChartControls'
174
+ import BottomFields from './BottomFields'
175
+ import SelectionBox from './SelectionBox'
176
+
177
+ import {
178
+ useTooltip,
179
+ useChartData,
180
+ useAxisCalculations,
181
+ useSelection,
182
+ useChartScroll,
183
+ } from './composables'
184
+
185
+ import {
186
+ Container,
187
+ GraphSection,
188
+ YAxis,
189
+ YAxisRow,
190
+ YAxisLabel,
191
+ YAxisLine,
192
+ YAxisTitleWrapper,
193
+ ScrollContainer,
194
+ ChartContent,
195
+ BarsContainer,
196
+ BarGroup,
197
+ BarWrapper,
198
+ BarSegment,
199
+ XAxisLabel,
200
+ XAxisLabelHighlight,
201
+ XAxisLine,
202
+ Tooltip,
203
+ TooltipText,
204
+ TooltipTextWrapper,
205
+ TooltipRow,
206
+ TooltipGradientBox,
207
+ ChartControlsWrapper,
208
+ } from './styles/chart'
209
+
210
+ const props = defineProps({
211
+ data: {
212
+ type: Array,
213
+ default: () => [],
214
+ validator: (value, ...args) => value.every((item) => 'label' in item),
215
+ },
216
+ series: {
217
+ type: Array,
218
+ default: () => [],
219
+ validator: (value) =>
220
+ value.every(
221
+ (item) => 'name' in item && 'data' in item && Array.isArray(item.data)
222
+ ),
223
+ },
224
+ width: {
225
+ type: String,
226
+ default: '100%',
227
+ },
228
+ height: {
229
+ type: String,
230
+ default: '400px',
231
+ },
232
+ barWidth: {
233
+ type: Number,
234
+ default: 60,
235
+ },
236
+ steps: {
237
+ type: Number,
238
+ default: null,
239
+ },
240
+ yAxisTitle: {
241
+ type: String,
242
+ default: '',
243
+ },
244
+ valueFormatter: {
245
+ type: Function,
246
+ default: null,
247
+ },
248
+ isLegendShown: {
249
+ type: Boolean,
250
+ default: false,
251
+ },
252
+ legendsItemPerRow: {
253
+ type: Number,
254
+ default: 4,
255
+ },
256
+ chartControlsPosition: {
257
+ type: String,
258
+ default: 'top',
259
+ validator: (value) => ['top', 'bottom'].includes(value),
260
+ },
261
+ splitButtonOptions: {
262
+ type: Array,
263
+ default: () => [],
264
+ },
265
+ selectedSplitButton: {
266
+ type: String,
267
+ default: '',
268
+ },
269
+ isScrollable: {
270
+ type: Boolean,
271
+ default: true,
272
+ },
273
+ isBottomFieldsShown: {
274
+ type: Boolean,
275
+ default: false,
276
+ },
277
+ selectionSize: {
278
+ type: Number,
279
+ default: 0,
280
+ },
281
+ isSelectionEnabled: {
282
+ type: Boolean,
283
+ default: false,
284
+ },
285
+ fieldMode: {
286
+ type: String,
287
+ default: 'absolute',
288
+ validator: (value) => ['absolute', 'percentage'].includes(value),
289
+ },
290
+ })
291
+
292
+ const generateChartId = () =>
293
+ `chart-${Date.now()}-${Math.floor(Math.random() * 1000)}`
294
+ const chartId = generateChartId()
295
+
296
+ const maxDataValue = computed(() => {
297
+ if (!props.data.length) return 0
298
+
299
+ return Math.max(
300
+ ...props.data.map((item) =>
301
+ props.series.length
302
+ ? props.series.reduce(
303
+ (sum, series) =>
304
+ sum +
305
+ (series.data.find((d) => d.label === item.label)?.value || 0),
306
+ 0
307
+ )
308
+ : item.value
309
+ )
310
+ )
311
+ })
312
+
313
+ const {
314
+ yAxisLabels,
315
+ yAxisHeight,
316
+ yAxisWidth,
317
+ isChartControlsShown,
318
+ paddedMaxValue,
319
+ } = useAxisCalculations(props, maxDataValue)
320
+
321
+ const { normalizedData, getStackedColors, getTotalSegmentValue } =
322
+ useChartData(props, paddedMaxValue)
323
+
324
+ const {
325
+ updateSelectedBars,
326
+ handleSelectionDragEnd,
327
+ isSelectionBoundary,
328
+ getSegmentGradient,
329
+ } = useSelection(props, normalizedData, emit)
330
+
331
+ const {
332
+ showTooltipContent,
333
+ showTooltip,
334
+ hideTooltip,
335
+ tooltipData,
336
+ tooltipStyle,
337
+ isInputFocused,
338
+ focusedBarData,
339
+ showTooltipFromInput,
340
+ handleInputBlurAll,
341
+ } = useTooltip(chartId, normalizedData)
342
+
343
+ const {
344
+ chartContent,
345
+ chartContentWidth,
346
+ handleChartScroll,
347
+ handleBottomFieldsScroll,
348
+ } = useChartScroll(
349
+ chartId,
350
+ isInputFocused,
351
+ focusedBarData,
352
+ showTooltipFromInput
353
+ )
354
+
355
+ const emit = defineEmits([
356
+ 'select-split-button',
357
+ 'selection-change',
358
+ 'input-blur',
359
+ ])
360
+
361
+ const slots = useSlots()
362
+
363
+ const handleSelectSplitButton = (value) => {
364
+ emit('select-split-button', value)
365
+ }
366
+
367
+ const handleInputBlur = (payload) => {
368
+ emit('input-blur', payload)
369
+ }
370
+
371
+ const handleValueFormatter = (value) => {
372
+ return props.valueFormatter
373
+ ? props.valueFormatter(Math.round(value))
374
+ : value
375
+ }
376
+ </script>
@@ -0,0 +1,66 @@
1
+ import styled from 'vue3-styled-components'
2
+
3
+ export const Container = styled('div', {
4
+ isChartControlsShownInBottom: Boolean,
5
+ })`
6
+ display: flex;
7
+ margin-top: ${(props) =>
8
+ props.isChartControlsShownInBottom ? '20px' : '44px'};
9
+ `
10
+
11
+ export const LabelsColumn = styled('div', { width: String })`
12
+ width: ${(props) => props.width};
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 12px;
16
+ `
17
+
18
+ export const LabelRow = styled.div`
19
+ height: 32px;
20
+ font-size: 12px;
21
+ font-weight: 500;
22
+ color: ${(props) => props.theme.semanticColors.teal[600]};
23
+ display: flex;
24
+ align-items: flex-start;
25
+ `
26
+
27
+ export const TotalRow = styled(LabelRow)``
28
+
29
+ export const FieldsContainer = styled.div`
30
+ flex: 1;
31
+ overflow-x: auto;
32
+ scrollbar-width: none;
33
+
34
+ &::-webkit-scrollbar {
35
+ display: none;
36
+ }
37
+ `
38
+
39
+ export const FieldsWrapper = styled.div`
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 8px;
43
+ `
44
+
45
+ export const InputRow = styled.div`
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: space-around;
49
+ gap: 8px;
50
+ padding-left: 12px;
51
+ padding-right: 12px;
52
+ `
53
+
54
+ export const TotalInputRow = styled(InputRow)`
55
+ margin-top: 0;
56
+ `
57
+
58
+ export const InputGroup = styled('div', {
59
+ barWidth: Number,
60
+ isScrollable: Boolean,
61
+ })`
62
+ ${(props) => (props.isScrollable ? 'min-width' : 'width')}:${(props) =>
63
+ props.barWidth}px;
64
+ display: flex;
65
+ justify-content: center;
66
+ `
@@ -0,0 +1,259 @@
1
+ import styled from 'vue3-styled-components'
2
+ import theme from '@/assets/theme'
3
+
4
+ export const Container = styled('div', { width: String })`
5
+ display: flex;
6
+ flex-direction: column;
7
+ padding-top: 40px;
8
+ font-family: ${(props) => props.theme.fonts.mainFont};
9
+ width: ${(props) => props.width};
10
+ `
11
+
12
+ export const GraphSection = styled('div', { width: String, height: String })`
13
+ height: ${(props) => props.height};
14
+ width: ${(props) => props.width};
15
+ position: relative;
16
+ display: flex;
17
+ `
18
+
19
+ export const YAxis = styled('div', { width: String, height: String })`
20
+ width: ${(props) => props.width};
21
+ display: flex;
22
+ flex-direction: column;
23
+ position: relative;
24
+ height: ${(props) => props.height};
25
+ `
26
+
27
+ export const YAxisRow = styled('div', { percentage: Number })`
28
+ display: flex;
29
+ align-items: center;
30
+ width: 100%;
31
+ position: absolute;
32
+ height: 0;
33
+ bottom: ${(props) =>
34
+ Number.isFinite(props.percentage) ? `${props.percentage}%` : '0'};
35
+ transform: translateY(50%);
36
+ `
37
+
38
+ export const YAxisLabel = styled.div`
39
+ font-size: 12px;
40
+ color: ${(props) => props.theme.semanticColors.teal[600]};
41
+ width: 100%;
42
+ text-align: right;
43
+ padding-right: 16px;
44
+ position: relative;
45
+ z-index: 1;
46
+ `
47
+
48
+ export const YAxisLine = styled('div', { yAxisWidth: String })`
49
+ position: absolute;
50
+ right: -10px;
51
+ left: calc(${(props) => props.yAxisWidth} - 10px);
52
+ height: 1px;
53
+ background-color: rgba(0, 0, 0, 0.1);
54
+ width: 12px;
55
+ z-index: 0;
56
+ top: 50%;
57
+ transform: translateY(-50%);
58
+ `
59
+
60
+ export const YAxisTitleWrapper = styled('div', { height: String })`
61
+ position: absolute;
62
+ left: -66px;
63
+ top: ${(props) => props.height};
64
+ transform: rotate(-90deg) translateX(50%);
65
+ transform-origin: right;
66
+ font-size: 12px;
67
+ color: ${(props) => props.theme.semanticColors.teal[600]};
68
+ display: flex;
69
+ align-items: center;
70
+ white-space: nowrap;
71
+ font-family: ${(props) => props.theme.fonts.mainFont};
72
+ `
73
+
74
+ export const ScrollContainer = styled('div', {
75
+ isScrollable: Boolean,
76
+ height: String,
77
+ })`
78
+ flex: 1;
79
+ overflow-x: auto;
80
+ overflow-y: hidden;
81
+ height: calc(${(props) => props.height} + 30px);
82
+ `
83
+
84
+ export const ChartContent = styled('div', {
85
+ totalBars: Number,
86
+ barWidth: Number,
87
+ isScrollable: Boolean,
88
+ height: String,
89
+ })`
90
+ height: ${(props) => props.height};
91
+ position: relative;
92
+ background: ${(props) => props.theme.semanticColors.grey[100]};
93
+ ${(props) =>
94
+ props.isScrollable
95
+ ? ` min-width: ${props.totalBars * (props.barWidth + 8) + 24}px;`
96
+ : 'width: 100%;'}
97
+ `
98
+
99
+ export const BarsContainer = styled.div`
100
+ height: 100%;
101
+ display: flex;
102
+ align-items: flex-end;
103
+ justify-content: space-around;
104
+ gap: 8px;
105
+ padding-left: 12px;
106
+ padding-right: 12px;
107
+ position: relative;
108
+ z-index: 1;
109
+ pointer-events: none;
110
+ `
111
+
112
+ export const BarGroup = styled('div', {
113
+ barWidth: Number,
114
+ isScrollable: Boolean,
115
+ })`
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ height: 100%;
120
+ position: relative;
121
+ ${(props) => (props.isScrollable ? 'min-width' : 'width')}:${(props) =>
122
+ props.barWidth}px;
123
+ pointer-events: none;
124
+ `
125
+
126
+ export const BarWrapper = styled.div`
127
+ height: 100%;
128
+ width: 100%;
129
+ position: relative;
130
+ `
131
+
132
+ export const BarSegment = styled('div', {
133
+ gradientFrom: String,
134
+ gradientTo: String,
135
+ height: String,
136
+ zIndex: Number,
137
+ })`
138
+ position: absolute;
139
+ bottom: 0;
140
+ left: 0;
141
+ right: 0;
142
+ height: ${(props) => props.height};
143
+ z-index: ${(props) => props.zIndex};
144
+ transition: opacity 0.2s;
145
+ border-radius: 8px 8px 0 0;
146
+ background: ${(props) =>
147
+ `linear-gradient(180deg, ${props.gradientFrom} 0%, ${props.gradientTo} 100%)`};
148
+ transform-origin: bottom;
149
+ will-change: transform, height;
150
+ pointer-events: auto;
151
+ &:hover {
152
+ opacity: 0.8;
153
+ }
154
+ `
155
+
156
+ export const XAxisLabel = styled.div`
157
+ font-size: 12px;
158
+ color: ${(props) => props.theme.semanticColors.teal[600]};
159
+ position: absolute;
160
+ bottom: -12px;
161
+ transform: translateY(100%);
162
+ user-select: none;
163
+ `
164
+
165
+ export const XAxisLabelHighlight = styled.div`
166
+ background: ${theme.semanticColors.purple[500]};
167
+ color: white;
168
+ padding: 4px;
169
+ border-radius: 4px;
170
+ font-size: 12px;
171
+ position: absolute;
172
+ bottom: -8px;
173
+ transform: translateY(100%);
174
+ user-select: none;
175
+ text-align: center;
176
+ `
177
+
178
+ export const XAxisLine = styled.div`
179
+ width: 1px;
180
+ height: 6px;
181
+ background-color: rgba(0, 0, 0, 0.1);
182
+ position: absolute;
183
+ bottom: -6px;
184
+ left: 50%;
185
+ transform: translateX(-50%);
186
+ `
187
+
188
+ export const Tooltip = styled('div', {
189
+ top: String,
190
+ left: String,
191
+ })`
192
+ position: fixed;
193
+ top: ${(props) => props.top};
194
+ left: ${(props) => props.left};
195
+ background: rgba(0, 0, 0, 0.8);
196
+ color: white;
197
+ padding: 4px 6px;
198
+ border-radius: 4px;
199
+ font-size: 14px;
200
+ pointer-events: none;
201
+ transform: translate(-50%, -100%);
202
+ z-index: 1000;
203
+ margin-top: -10px;
204
+
205
+ &::after {
206
+ content: '';
207
+ position: absolute;
208
+ bottom: -6px;
209
+ left: 50%;
210
+ transform: translateX(-50%);
211
+ border-left: 4px solid transparent;
212
+ border-right: 4px solid transparent;
213
+ border-top: 6px solid rgba(0, 0, 0, 0.8);
214
+ width: 0;
215
+ height: 0;
216
+ }
217
+ `
218
+
219
+ export const TooltipText = styled('div', {
220
+ fontWeight: String,
221
+ })`
222
+ font-weight: ${(props) => props.fontWeight || '400'};
223
+ font-size: 12px;
224
+ `
225
+
226
+ export const TooltipTextWrapper = styled.div`
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 4px;
230
+ `
231
+
232
+ export const TooltipRow = styled.div`
233
+ width: 100px;
234
+ display: flex;
235
+ flex-direction: row;
236
+ justify-content: space-between;
237
+ align-items: center;
238
+ `
239
+
240
+ export const TooltipGradientBox = styled('div', {
241
+ gradientFrom: String,
242
+ gradientTo: String,
243
+ })`
244
+ background: ${(props) =>
245
+ `linear-gradient(180deg, ${props.gradientFrom} 0%, ${props.gradientTo} 100%)`};
246
+ width: 12px;
247
+ height: 12px;
248
+ border-radius: 4px;
249
+ `
250
+
251
+ export const ChartControlsWrapper = styled('div', { position: String })`
252
+ ${(props) =>
253
+ props.position === 'top' ? 'margin-bottom: 6px;' : 'margin-top: 36px;'}
254
+ `
255
+
256
+ export const BottomFieldsContainer = styled.div`
257
+ margin-top: 16px;
258
+ width: 100%;
259
+ `