@eturnity/eturnity_reusable_components 8.16.9-EPDM-15095.0 → 8.16.9-EPDM-14690.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/package.json +1 -1
- package/src/TestChart.vue +229 -0
- package/src/components/barchart/BottomFields.vue +154 -31
- package/src/components/barchart/composables/useAxisCalculations.js +1 -1
- package/src/components/barchart/composables/useChartData.js +1 -1
- package/src/components/barchart/composables/useTooltip.js +28 -3
- package/src/components/barchart/index.vue +27 -11
- package/src/components/barchart/styles/bottomFields.js +7 -2
- package/src/components/barchart/styles/chart.js +1 -0
- package/src/components/errorMessage/index.vue +1 -1
- package/src/components/infoCard/index.vue +3 -7
- package/src/components/inputs/inputNumber/index.vue +14 -4
- package/src/components/pageTitle/index.vue +3 -4
package/package.json
CHANGED
@@ -0,0 +1,229 @@
|
|
1
|
+
<template>
|
2
|
+
<div
|
3
|
+
style="
|
4
|
+
margin-top: 100px;
|
5
|
+
margin-left: 80px;
|
6
|
+
padding-bottom: 100px;
|
7
|
+
display: flex;
|
8
|
+
flex-direction: column;
|
9
|
+
"
|
10
|
+
>
|
11
|
+
<!-- Simple bar chart -->
|
12
|
+
<BarChart
|
13
|
+
:bar-width="60"
|
14
|
+
chart-controls-position="bottom"
|
15
|
+
:data="monthlyData"
|
16
|
+
height="400px"
|
17
|
+
:is-bottom-fields-shown="true"
|
18
|
+
:is-scrollable="false"
|
19
|
+
:is-selection-enabled="true"
|
20
|
+
:selection-size="3"
|
21
|
+
:value-formatter="valueFormatter"
|
22
|
+
width="700px"
|
23
|
+
@selection-change="handleSelectionChange"
|
24
|
+
/>
|
25
|
+
<br />
|
26
|
+
<br />
|
27
|
+
|
28
|
+
<!-- Stacked bar chart -->
|
29
|
+
<BarChart
|
30
|
+
:bar-width="60"
|
31
|
+
:data="monthLabels"
|
32
|
+
height="400px"
|
33
|
+
:is-bottom-fields-shown="true"
|
34
|
+
:is-legend-shown="true"
|
35
|
+
:legends-item-per-row="4"
|
36
|
+
:selected-split-button="selectedTimeFrame"
|
37
|
+
:series="tariffZones"
|
38
|
+
:show-percentage-on-tooltip="true"
|
39
|
+
:split-button-options="options"
|
40
|
+
:value-formatter="valueFormatter"
|
41
|
+
width="700px"
|
42
|
+
y-axis-title="Energy (kWh)"
|
43
|
+
@input-blur="handleInputBlur"
|
44
|
+
@select-split-button="handleSelectSplitButton"
|
45
|
+
/>
|
46
|
+
|
47
|
+
<!-- Stacked bar chart -->
|
48
|
+
<BarChart
|
49
|
+
:bar-width="60"
|
50
|
+
:data="monthLabels"
|
51
|
+
field-mode="percentage"
|
52
|
+
height="400px"
|
53
|
+
:is-bottom-fields-shown="true"
|
54
|
+
:is-legend-shown="false"
|
55
|
+
:legends-item-per-row="4"
|
56
|
+
:series="tariffZones"
|
57
|
+
:value-formatter="valueFormatter"
|
58
|
+
width="700px"
|
59
|
+
@input-blur="handleInputBlur"
|
60
|
+
@select-split-button="handleSelectSplitButton"
|
61
|
+
>
|
62
|
+
<!-- <template #tooltip="{ item, segment }">
|
63
|
+
<div style="display: flex; flex-direction: column">
|
64
|
+
{{ $c.log(item, segment) }}
|
65
|
+
<div>{{ item.label }}</div>
|
66
|
+
<div>{{ item.segments[0].value }} kWh</div>
|
67
|
+
</div>
|
68
|
+
</template> -->
|
69
|
+
</BarChart>
|
70
|
+
</div>
|
71
|
+
</template>
|
72
|
+
|
73
|
+
<script setup>
|
74
|
+
import { ref } from 'vue'
|
75
|
+
import BarChart from '@/components/barchart/index.vue'
|
76
|
+
|
77
|
+
const options = [
|
78
|
+
{ label: 'Day', value: 'day' },
|
79
|
+
{ label: 'Month', value: 'month' },
|
80
|
+
{ label: 'Year', value: 'year' },
|
81
|
+
]
|
82
|
+
|
83
|
+
const selectedTimeFrame = ref('day')
|
84
|
+
|
85
|
+
const handleSelectSplitButton = (value) => {
|
86
|
+
selectedTimeFrame.value = value
|
87
|
+
}
|
88
|
+
|
89
|
+
const monthlyData = [
|
90
|
+
{ label: 'Jan', value: 300 },
|
91
|
+
{ label: 'Feb', value: 600 },
|
92
|
+
{ label: 'Mar', value: 1000 },
|
93
|
+
{ label: 'Apr', value: 1200 },
|
94
|
+
{ label: 'May', value: 1400 },
|
95
|
+
{ label: 'Jun', value: 1810 },
|
96
|
+
{ label: 'Jul', value: 1400 },
|
97
|
+
{ label: 'Aug', value: 1200 },
|
98
|
+
{ label: 'Sep', value: 1000 },
|
99
|
+
// { label: 'Oct', value: 800 },
|
100
|
+
// { label: 'Nov', value: 600 },
|
101
|
+
// { label: 'Dec', value: 400 },
|
102
|
+
// { label: 'Jan', value: 300 },
|
103
|
+
// { label: 'Feb', value: 600 },
|
104
|
+
// { label: 'Mar', value: 1000 },
|
105
|
+
// { label: 'Apr', value: 1200 },
|
106
|
+
// { label: 'May', value: 1400 },
|
107
|
+
// { label: 'Jun', value: 1810 },
|
108
|
+
// { label: 'Jul', value: 1400 },
|
109
|
+
// { label: 'Aug', value: 1200 },
|
110
|
+
// { label: 'Sep', value: 1000 },
|
111
|
+
// { label: 'Oct', value: 800 },
|
112
|
+
// { label: 'Nov', value: 600 },
|
113
|
+
// { label: 'Dec', value: 400 },
|
114
|
+
|
115
|
+
// ... more months
|
116
|
+
]
|
117
|
+
|
118
|
+
const monthLabels = [
|
119
|
+
{ label: 'Jan' },
|
120
|
+
{ label: 'Feb' },
|
121
|
+
{ label: 'Mar' },
|
122
|
+
{ label: 'Apr' },
|
123
|
+
{ label: 'May' },
|
124
|
+
{ label: 'Jun' },
|
125
|
+
// ... more months
|
126
|
+
]
|
127
|
+
|
128
|
+
const tariffZones = ref([
|
129
|
+
{
|
130
|
+
name: 'Tariff Zone 1',
|
131
|
+
data: [
|
132
|
+
{ label: 'Jan', value: 200 },
|
133
|
+
{ label: 'Feb', value: 130 },
|
134
|
+
{ label: 'Mar', value: 220 },
|
135
|
+
{ label: 'Apr', value: 230 },
|
136
|
+
{ label: 'May', value: 200 },
|
137
|
+
{ label: 'Jun', value: 210 },
|
138
|
+
// ... more months
|
139
|
+
],
|
140
|
+
},
|
141
|
+
{
|
142
|
+
name: 'Tariff Zone 2',
|
143
|
+
data: [
|
144
|
+
{ label: 'Jan', value: 200 },
|
145
|
+
{ label: 'Feb', value: 100 },
|
146
|
+
{ label: 'Mar', value: 270 },
|
147
|
+
{ label: 'Apr', value: 180 },
|
148
|
+
{ label: 'May', value: 300 },
|
149
|
+
{ label: 'Jun', value: 250 },
|
150
|
+
// ... more months
|
151
|
+
],
|
152
|
+
},
|
153
|
+
{
|
154
|
+
name: 'Tariff Zone 3',
|
155
|
+
data: [
|
156
|
+
{ label: 'Jan', value: 200 },
|
157
|
+
{ label: 'Feb', value: 100 },
|
158
|
+
{ label: 'Mar', value: 210 },
|
159
|
+
{ label: 'Apr', value: 220 },
|
160
|
+
{ label: 'May', value: 300 },
|
161
|
+
{ label: 'Jun', value: 190 },
|
162
|
+
// ... more months
|
163
|
+
],
|
164
|
+
},
|
165
|
+
{
|
166
|
+
name: 'Tariff Zone 4',
|
167
|
+
data: [
|
168
|
+
{ label: 'Jan', value: 200 },
|
169
|
+
{ label: 'Feb', value: 100 },
|
170
|
+
{ label: 'Mar', value: 210 },
|
171
|
+
{ label: 'Apr', value: 220 },
|
172
|
+
{ label: 'May', value: 300 },
|
173
|
+
{ label: 'Jun', value: 190 },
|
174
|
+
// ... more months
|
175
|
+
],
|
176
|
+
},
|
177
|
+
{
|
178
|
+
name: 'Tariff Zone 5',
|
179
|
+
data: [
|
180
|
+
{ label: 'Jan', value: 200 },
|
181
|
+
{ label: 'Feb', value: 100 },
|
182
|
+
{ label: 'Mar', value: 210 },
|
183
|
+
{ label: 'Apr', value: 220 },
|
184
|
+
{ label: 'May', value: 300 },
|
185
|
+
{ label: 'Jun', value: 190 },
|
186
|
+
// ... more months
|
187
|
+
],
|
188
|
+
},
|
189
|
+
{
|
190
|
+
name: 'Tariff Zone 6',
|
191
|
+
data: [
|
192
|
+
{ label: 'Jan', value: 200 },
|
193
|
+
{ label: 'Feb', value: 100 },
|
194
|
+
{ label: 'Mar', value: 210 },
|
195
|
+
{ label: 'Apr', value: 220 },
|
196
|
+
{ label: 'May', value: 300 },
|
197
|
+
{ label: 'Jun', value: 190 },
|
198
|
+
// ... more months
|
199
|
+
],
|
200
|
+
},
|
201
|
+
// ... more tariff zones
|
202
|
+
])
|
203
|
+
|
204
|
+
const valueFormatter = (value) => {
|
205
|
+
return `${value} kWh`
|
206
|
+
}
|
207
|
+
|
208
|
+
const handleSelectionChange = (selectedBars) => {
|
209
|
+
console.log('selectedBars', selectedBars)
|
210
|
+
}
|
211
|
+
|
212
|
+
const handleInputBlur = (payload) => {
|
213
|
+
const newVal = [...tariffZones.value].map((zone) => {
|
214
|
+
if (zone.name === payload.seriesName) {
|
215
|
+
zone.data = zone.data.map((item) => {
|
216
|
+
if (item.label === payload.label) {
|
217
|
+
item.value = payload.value
|
218
|
+
}
|
219
|
+
return item
|
220
|
+
})
|
221
|
+
}
|
222
|
+
return zone
|
223
|
+
})
|
224
|
+
|
225
|
+
tariffZones.value = newVal
|
226
|
+
}
|
227
|
+
</script>
|
228
|
+
|
229
|
+
<style lang="scss" scoped></style>
|
@@ -1,14 +1,14 @@
|
|
1
1
|
<template>
|
2
2
|
<Container :is-chart-controls-shown-in-bottom="isChartControlsShownInBottom">
|
3
3
|
<LabelsColumn :width="yAxisWidth">
|
4
|
-
<LabelRow v-for="series in
|
4
|
+
<LabelRow v-for="series in seriesData" :key="series.name">
|
5
5
|
{{ series.name }}
|
6
6
|
</LabelRow>
|
7
|
-
<TotalRow v-if="
|
8
|
-
{{ $gettext ? $gettext('Total (%)
|
7
|
+
<TotalRow v-if="seriesData.length && fieldMode === 'percentage'">
|
8
|
+
{{ $gettext ? `${$gettext('Total')} (%)` : 'Total (%)' }}
|
9
9
|
</TotalRow>
|
10
|
-
<TotalRow v-if="
|
11
|
-
{{ $gettext ? $gettext('Total (kWh)
|
10
|
+
<TotalRow v-if="seriesData.length">
|
11
|
+
{{ $gettext ? `${$gettext('Total')} (kWh)` : 'Total (kWh)' }}
|
12
12
|
</TotalRow>
|
13
13
|
</LabelsColumn>
|
14
14
|
|
@@ -18,27 +18,37 @@
|
|
18
18
|
>
|
19
19
|
<FieldsWrapper>
|
20
20
|
<!-- For stacked bar chart -->
|
21
|
-
<template v-if="
|
21
|
+
<template v-if="seriesData.length">
|
22
22
|
<InputRow
|
23
|
-
v-for="series in
|
23
|
+
v-for="series in seriesData"
|
24
24
|
:key="series.name"
|
25
25
|
:data-series-name="series.name"
|
26
26
|
>
|
27
27
|
<InputGroup
|
28
28
|
v-for="(item, index) in props.data"
|
29
|
+
:key="index"
|
29
30
|
:bar-width="barWidth"
|
30
31
|
:is-scrollable="isScrollable"
|
31
|
-
:key="index"
|
32
32
|
>
|
33
33
|
<InputNumber
|
34
34
|
:allow-negative="false"
|
35
|
+
:disabled="isInputsDisabled"
|
36
|
+
:error-message="null"
|
35
37
|
input-height="36px"
|
36
|
-
:
|
38
|
+
:is-border-error-only="true"
|
39
|
+
:is-error="
|
40
|
+
fieldMode === 'percentage'
|
41
|
+
? calculatePercentageTotal(item.label) !== 100
|
42
|
+
: false
|
43
|
+
"
|
37
44
|
:min-decimals="0"
|
45
|
+
:number-precision="fieldMode === 'percentage' ? 2 : 0"
|
38
46
|
text-align="center"
|
39
47
|
:unit-name="fieldMode === 'percentage' ? '%' : ''"
|
40
48
|
:value="getDisplayValue(series.data, item.label)"
|
41
|
-
@input-blur="
|
49
|
+
@input-blur="
|
50
|
+
handleInputBlur($event, series.name, item.label, series.data)
|
51
|
+
"
|
42
52
|
@input-focus="handleInputFocus(series.name, item.label)"
|
43
53
|
/>
|
44
54
|
</InputGroup>
|
@@ -47,16 +57,17 @@
|
|
47
57
|
<TotalInputRow v-if="fieldMode === 'percentage'">
|
48
58
|
<InputGroup
|
49
59
|
v-for="(item, index) in props.data"
|
60
|
+
:key="index"
|
50
61
|
:bar-width="barWidth"
|
51
62
|
:is-scrollable="isScrollable"
|
52
|
-
:key="index"
|
53
63
|
>
|
54
64
|
<InputNumber
|
55
65
|
:allow-negative="false"
|
66
|
+
:disabled="isInputsDisabled"
|
56
67
|
input-height="36px"
|
57
68
|
:is-read-only="true"
|
58
|
-
:number-precision="0"
|
59
69
|
:min-decimals="0"
|
70
|
+
:number-precision="fieldMode === 'percentage' ? 2 : 0"
|
60
71
|
text-align="center"
|
61
72
|
:unit-name="fieldMode === 'percentage' ? '%' : ''"
|
62
73
|
:value="calculatePercentageTotal(item.label)"
|
@@ -67,17 +78,24 @@
|
|
67
78
|
<TotalInputRow>
|
68
79
|
<InputGroup
|
69
80
|
v-for="(item, index) in props.data"
|
81
|
+
:key="index"
|
70
82
|
:bar-width="barWidth"
|
71
83
|
:is-scrollable="isScrollable"
|
72
|
-
:key="index"
|
73
84
|
>
|
74
85
|
<InputNumber
|
86
|
+
:error-message="$gettext('load_profile_not_add_up_to_100')"
|
75
87
|
input-height="36px"
|
88
|
+
:is-border-error-only="true"
|
89
|
+
:is-error="
|
90
|
+
fieldMode === 'percentage'
|
91
|
+
? calculatePercentageTotal(item.label) !== 100
|
92
|
+
: false
|
93
|
+
"
|
76
94
|
:is-read-only="true"
|
77
|
-
:number-precision="2"
|
78
95
|
:min-decimals="0"
|
96
|
+
:number-precision="2"
|
79
97
|
text-align="center"
|
80
|
-
:value="
|
98
|
+
:value="calculateTotalValue(item.label)"
|
81
99
|
/>
|
82
100
|
</InputGroup>
|
83
101
|
</TotalInputRow>
|
@@ -88,17 +106,19 @@
|
|
88
106
|
<InputRow>
|
89
107
|
<InputGroup
|
90
108
|
v-for="(item, index) in props.data"
|
109
|
+
:key="index"
|
91
110
|
:bar-width="barWidth"
|
92
111
|
:is-scrollable="isScrollable"
|
93
|
-
:key="index"
|
94
112
|
>
|
95
113
|
<InputNumber
|
114
|
+
:allow-negative="false"
|
115
|
+
:disabled="isInputsDisabled"
|
96
116
|
input-height="36px"
|
97
117
|
:min-decimals="0"
|
98
118
|
:number-precision="2"
|
99
119
|
text-align="center"
|
100
120
|
:value="item.value"
|
101
|
-
@input-blur="handleInputBlur($event, null, item.label)"
|
121
|
+
@input-blur="handleInputBlur($event, null, item.label, null)"
|
102
122
|
@input-focus="handleInputFocus(null, item.label)"
|
103
123
|
/>
|
104
124
|
</InputGroup>
|
@@ -110,7 +130,8 @@
|
|
110
130
|
</template>
|
111
131
|
|
112
132
|
<script setup>
|
113
|
-
import { ref } from 'vue'
|
133
|
+
import { ref, watch, watchEffect, onMounted } from 'vue'
|
134
|
+
import styled from 'vue3-styled-components'
|
114
135
|
import InputNumber from '../inputs/inputNumber'
|
115
136
|
|
116
137
|
import {
|
@@ -159,6 +180,61 @@
|
|
159
180
|
default: 'absolute',
|
160
181
|
validator: (value) => ['absolute', 'percentage'].includes(value),
|
161
182
|
},
|
183
|
+
isInputsDisabled: {
|
184
|
+
type: Boolean,
|
185
|
+
default: false,
|
186
|
+
},
|
187
|
+
})
|
188
|
+
|
189
|
+
const seriesData = ref([])
|
190
|
+
|
191
|
+
// onMounted(() => {
|
192
|
+
// seriesData.value = props.series.map((item) => {
|
193
|
+
// const data = item.data.map((d) => ({
|
194
|
+
// label: d.label,
|
195
|
+
// value: d.value,
|
196
|
+
// originalValue: d.value,
|
197
|
+
// }))
|
198
|
+
|
199
|
+
// return {
|
200
|
+
// name: item.name,
|
201
|
+
// data,
|
202
|
+
// }
|
203
|
+
// })
|
204
|
+
// })
|
205
|
+
|
206
|
+
watchEffect(() => {
|
207
|
+
let isNewSetOfSeries = false
|
208
|
+
const seriesDataCopy = [...seriesData.value]
|
209
|
+
if (
|
210
|
+
!seriesDataCopy.length ||
|
211
|
+
!props.series.length ||
|
212
|
+
seriesDataCopy.length !== props.series.length ||
|
213
|
+
!seriesDataCopy.some((item) => {
|
214
|
+
return props.series.map((s) => s.name).includes(item.name)
|
215
|
+
})
|
216
|
+
) {
|
217
|
+
isNewSetOfSeries = true
|
218
|
+
}
|
219
|
+
const currentSeriesData = !isNewSetOfSeries ? seriesDataCopy : []
|
220
|
+
const newSeriesData = []
|
221
|
+
|
222
|
+
props.series.forEach((item, itemIndex) => {
|
223
|
+
const data = item.data.map((d, dIndex) => ({
|
224
|
+
label: d.label,
|
225
|
+
value: d.value,
|
226
|
+
originalValue: currentSeriesData.length
|
227
|
+
? currentSeriesData[itemIndex].data[dIndex].originalValue
|
228
|
+
: d.value,
|
229
|
+
}))
|
230
|
+
|
231
|
+
newSeriesData.push({
|
232
|
+
name: item.name,
|
233
|
+
data,
|
234
|
+
})
|
235
|
+
})
|
236
|
+
|
237
|
+
seriesData.value = [...newSeriesData]
|
162
238
|
})
|
163
239
|
|
164
240
|
const emit = defineEmits([
|
@@ -175,11 +251,13 @@
|
|
175
251
|
emit('input-focus', { seriesName, label })
|
176
252
|
}
|
177
253
|
|
178
|
-
const
|
179
|
-
|
254
|
+
const calculateTotalValue = (label) => {
|
255
|
+
const total = seriesData.value.reduce((sum, series) => {
|
180
256
|
const value = series.data.find((d) => d.label === label)?.value || 0
|
181
257
|
return sum + value
|
182
258
|
}, 0)
|
259
|
+
|
260
|
+
return Math.round(total)
|
183
261
|
}
|
184
262
|
|
185
263
|
const syncScroll = (scrollLeft) => {
|
@@ -190,34 +268,74 @@
|
|
190
268
|
container.scrollLeft = scrollLeft
|
191
269
|
}
|
192
270
|
}
|
193
|
-
|
271
|
+
|
272
|
+
const calculateTotalOriginalValue = (label) => {
|
273
|
+
return seriesData.value.reduce((sum, series) => {
|
274
|
+
const value =
|
275
|
+
series.data.find((d) => d.label === label)?.originalValue || 0
|
276
|
+
return sum + value
|
277
|
+
}, 0)
|
278
|
+
}
|
279
|
+
|
280
|
+
const getDisplayValue = (data, label, shouldRound = true) => {
|
194
281
|
if (props.fieldMode === 'absolute') {
|
195
|
-
return
|
282
|
+
return data.find((d) => d.label === label)?.value || ''
|
196
283
|
}
|
197
284
|
|
198
|
-
const value =
|
199
|
-
|
200
|
-
|
285
|
+
const value = data.find((d) => d.label === label)?.value || 0
|
286
|
+
|
287
|
+
const total = seriesData.value.reduce((sum, series) => {
|
288
|
+
const value =
|
289
|
+
series.data.find((d) => d.label === label)?.originalValue || 0
|
290
|
+
return sum + value
|
291
|
+
}, 0)
|
292
|
+
|
293
|
+
return shouldRound ? (value / total) * 100 : (value / total) * 100
|
201
294
|
}
|
202
295
|
|
203
296
|
const calculatePercentageTotal = (label) => {
|
204
|
-
|
297
|
+
const originalTotal = seriesData.value.reduce((sum, series) => {
|
298
|
+
const originalValue =
|
299
|
+
series.data.find((d) => d.label === label)?.originalValue || 0
|
300
|
+
return sum + originalValue
|
301
|
+
}, 0)
|
302
|
+
|
303
|
+
const totalPercentage = seriesData.value.reduce((sum, series) => {
|
205
304
|
const value = series.data.find((d) => d.label === label)?.value || 0
|
206
|
-
const
|
207
|
-
|
305
|
+
const percentage = originalTotal
|
306
|
+
? Number((value / originalTotal) * 100)
|
307
|
+
: 0
|
208
308
|
return sum + percentage
|
209
309
|
}, 0)
|
310
|
+
|
311
|
+
return Math.round(totalPercentage)
|
210
312
|
}
|
211
313
|
|
212
|
-
const handleInputBlur = (_value, seriesName, label) => {
|
314
|
+
const handleInputBlur = (_value, seriesName, label, currentSeriesData) => {
|
213
315
|
let value = Number(_value)
|
214
316
|
|
215
317
|
if (props.fieldMode === 'percentage') {
|
216
|
-
const total =
|
318
|
+
const total = seriesData.value.reduce((sum, series) => {
|
319
|
+
const value =
|
320
|
+
series.data.find((d) => d.label === label)?.originalValue || 0
|
321
|
+
return sum + value
|
322
|
+
}, 0)
|
323
|
+
|
217
324
|
value = (value / 100) * total
|
218
325
|
}
|
219
326
|
|
220
|
-
const payload = seriesName
|
327
|
+
const payload = seriesName
|
328
|
+
? {
|
329
|
+
seriesName,
|
330
|
+
label,
|
331
|
+
value,
|
332
|
+
inputValue: _value,
|
333
|
+
percentage: Number(
|
334
|
+
getDisplayValue(currentSeriesData, label, false).toFixed(2)
|
335
|
+
),
|
336
|
+
totalSeriesValue: calculateTotalOriginalValue(label),
|
337
|
+
}
|
338
|
+
: { label, value }
|
221
339
|
emit('input-blur', payload)
|
222
340
|
focusedInput.value = null
|
223
341
|
|
@@ -247,6 +365,11 @@
|
|
247
365
|
emit('sync-scroll', event.target.scrollLeft)
|
248
366
|
}
|
249
367
|
|
368
|
+
const toFixedNoRounding = (number, decimals) => {
|
369
|
+
const factor = Math.pow(10, decimals)
|
370
|
+
return Math.floor(number * factor) / factor
|
371
|
+
}
|
372
|
+
|
250
373
|
defineExpose({
|
251
374
|
syncScroll,
|
252
375
|
})
|
@@ -84,7 +84,7 @@ export function useAxisCalculations(props, maxValue) {
|
|
84
84
|
})
|
85
85
|
|
86
86
|
const yAxisWidth = computed(() => {
|
87
|
-
return !!props.yAxisTitle || props.isBottomFieldsShown ? '
|
87
|
+
return !!props.yAxisTitle || props.isBottomFieldsShown ? '80px' : '60px'
|
88
88
|
})
|
89
89
|
|
90
90
|
const isChartControlsShown = (position) => {
|
@@ -66,7 +66,7 @@ export function useChartData(props, paddedMaxValue) {
|
|
66
66
|
let accumulated = 0
|
67
67
|
return {
|
68
68
|
label: item.label,
|
69
|
-
segments: [...props.series].
|
69
|
+
segments: [...props.series].map((series, index) => {
|
70
70
|
const value =
|
71
71
|
series.data.find((d) => d.label === item.label)?.value || 0
|
72
72
|
accumulated += value
|
@@ -16,7 +16,21 @@ export function useTooltip(chartId, normalizedData) {
|
|
16
16
|
if (!showTooltipContent.value) {
|
17
17
|
showTooltipContent.value = true
|
18
18
|
}
|
19
|
-
if (isObjectEqual(item, tooltipData.value))
|
19
|
+
if (isObjectEqual(item, tooltipData.value)) {
|
20
|
+
return
|
21
|
+
}
|
22
|
+
|
23
|
+
const totalValue = item.segments.reduce((acc, segment) => {
|
24
|
+
return acc + segment.value
|
25
|
+
}, 0)
|
26
|
+
|
27
|
+
const segments = item.segments.map((segment) => {
|
28
|
+
let valuePercentage = (segment.value / totalValue) * 100
|
29
|
+
segment.valuePercentage = Math.round(valuePercentage)
|
30
|
+
|
31
|
+
return segment
|
32
|
+
})
|
33
|
+
item.segments = segments
|
20
34
|
|
21
35
|
tooltipData.value = { ...item }
|
22
36
|
|
@@ -41,16 +55,27 @@ export function useTooltip(chartId, normalizedData) {
|
|
41
55
|
|
42
56
|
isInputFocused.value = true
|
43
57
|
const barData = normalizedData.value.find((item) => item.label === label)
|
44
|
-
|
45
58
|
if (!barData) return
|
59
|
+
|
60
|
+
const totalValue = barData.segments.reduce((acc, segment) => {
|
61
|
+
return acc + segment.value
|
62
|
+
}, 0)
|
63
|
+
const segments = barData.segments.map((segment) => {
|
64
|
+
let valuePercentage = (segment.value / totalValue) * 100
|
65
|
+
segment.valuePercentage = Math.round(valuePercentage)
|
66
|
+
|
67
|
+
return segment
|
68
|
+
})
|
69
|
+
barData.segments = segments
|
46
70
|
focusedBarData.value = barData
|
71
|
+
|
47
72
|
const barElement = document.querySelector(
|
48
73
|
`.barchart-${chartId} .bar-group:nth-child(${
|
49
74
|
normalizedData.value.indexOf(barData) + 1
|
50
75
|
})`
|
51
76
|
)
|
52
|
-
|
53
77
|
if (!barElement) return
|
78
|
+
|
54
79
|
// Get the last bar segment, samee as hover behavior
|
55
80
|
const targetElement = barElement.querySelector('.bar-segment:last-child')
|
56
81
|
const rect = targetElement.getBoundingClientRect()
|
@@ -17,7 +17,7 @@
|
|
17
17
|
/>
|
18
18
|
</ChartControlsWrapper>
|
19
19
|
<GraphSection :height="height" :width="width">
|
20
|
-
<YAxis :
|
20
|
+
<YAxis :height="height" :width="yAxisWidth">
|
21
21
|
<YAxisTitleWrapper v-if="yAxisTitle" :height="yAxisHeight">
|
22
22
|
{{ yAxisTitle }}
|
23
23
|
</YAxisTitleWrapper>
|
@@ -30,15 +30,15 @@
|
|
30
30
|
)
|
31
31
|
"
|
32
32
|
>
|
33
|
-
<YAxisLabel>{{ label }}</YAxisLabel>
|
33
|
+
<YAxisLabel>{{ getYAxisLabel(label) }}</YAxisLabel>
|
34
34
|
<YAxisLine :y-axis-width="yAxisWidth" />
|
35
35
|
</YAxisRow>
|
36
36
|
</YAxis>
|
37
37
|
|
38
38
|
<ScrollContainer
|
39
39
|
:class="`chart-scroll-container-${chartId}`"
|
40
|
-
:is-scrollable="isScrollable"
|
41
40
|
:height="height"
|
41
|
+
:is-scrollable="isScrollable"
|
42
42
|
@scroll="handleChartScroll"
|
43
43
|
>
|
44
44
|
<ChartContent
|
@@ -64,19 +64,19 @@
|
|
64
64
|
<BarsContainer>
|
65
65
|
<BarGroup
|
66
66
|
v-for="(item, index) in normalizedData"
|
67
|
+
:key="index"
|
67
68
|
:bar-width="barWidth"
|
68
69
|
class="bar-group"
|
69
70
|
:is-scrollable="isScrollable"
|
70
|
-
:key="index"
|
71
71
|
>
|
72
72
|
<BarWrapper>
|
73
73
|
<BarSegment
|
74
74
|
v-for="(segment, segIndex) in item.segments"
|
75
|
+
:key="segIndex"
|
75
76
|
class="bar-segment"
|
76
77
|
:gradient-from="getSegmentGradient(index, segment).from"
|
77
78
|
:gradient-to="getSegmentGradient(index, segment).to"
|
78
79
|
:height="`${segment.percentage}%`"
|
79
|
-
:key="segIndex"
|
80
80
|
:z-index="item.segments.length - segIndex"
|
81
81
|
@mouseenter="showTooltip(item, $event, series)"
|
82
82
|
@mouseleave="hideTooltip"
|
@@ -96,7 +96,7 @@
|
|
96
96
|
:left="tooltipStyle.left"
|
97
97
|
:top="tooltipStyle.top"
|
98
98
|
>
|
99
|
-
<slot :item="tooltipData" name="tooltip"
|
99
|
+
<slot :item="tooltipData" name="tooltip"></slot>
|
100
100
|
<TooltipTextWrapper v-if="!slots.tooltip && tooltipData">
|
101
101
|
<template v-if="!series.length">
|
102
102
|
<TooltipText font-weight="500">{{ tooltipData.label }}</TooltipText>
|
@@ -128,7 +128,11 @@
|
|
128
128
|
:gradient-to="segment.gradientTo"
|
129
129
|
/>
|
130
130
|
<TooltipText>
|
131
|
-
{{
|
131
|
+
{{
|
132
|
+
fieldMode === 'absolute' && showPercentageOnTooltip
|
133
|
+
? `${segment.valuePercentage}%`
|
134
|
+
: handleValueFormatter(segment.value)
|
135
|
+
}}
|
132
136
|
</TooltipText>
|
133
137
|
</TooltipRow>
|
134
138
|
</template>
|
@@ -159,6 +163,7 @@
|
|
159
163
|
:data="data"
|
160
164
|
:field-mode="fieldMode"
|
161
165
|
:is-chart-controls-shown-in-bottom="isChartControlsShown('bottom')"
|
166
|
+
:is-inputs-disabled="isLoading"
|
162
167
|
:is-scrollable="isScrollable"
|
163
168
|
:series="series"
|
164
169
|
:y-axis-width="yAxisWidth"
|
@@ -171,12 +176,13 @@
|
|
171
176
|
</template>
|
172
177
|
|
173
178
|
<script setup>
|
174
|
-
import { useSlots, computed } from 'vue'
|
179
|
+
import { useSlots, computed, ref } from 'vue'
|
175
180
|
|
176
181
|
import ChartControls from './ChartControls'
|
177
182
|
import BottomFields from './BottomFields'
|
178
183
|
import SelectionBox from './SelectionBox'
|
179
184
|
import Spinner from '../spinner'
|
185
|
+
import { numberToString } from '../../helpers/numberConverter'
|
180
186
|
|
181
187
|
import {
|
182
188
|
useTooltip,
|
@@ -296,6 +302,10 @@
|
|
296
302
|
type: Boolean,
|
297
303
|
default: false,
|
298
304
|
},
|
305
|
+
showPercentageOnTooltip: {
|
306
|
+
type: Boolean,
|
307
|
+
default: false,
|
308
|
+
},
|
299
309
|
})
|
300
310
|
|
301
311
|
const generateChartId = () =>
|
@@ -378,8 +388,14 @@
|
|
378
388
|
}
|
379
389
|
|
380
390
|
const handleValueFormatter = (value) => {
|
381
|
-
return props.valueFormatter
|
382
|
-
|
383
|
-
|
391
|
+
return props.valueFormatter ? props.valueFormatter(value) : value
|
392
|
+
}
|
393
|
+
|
394
|
+
const getYAxisLabel = (label) => {
|
395
|
+
return numberToString({
|
396
|
+
value: label,
|
397
|
+
numberPrecision: 0,
|
398
|
+
minDecimals: 0,
|
399
|
+
})
|
384
400
|
}
|
385
401
|
</script>
|
@@ -17,18 +17,18 @@ export const LabelsColumn = styled('div', { width: String })`
|
|
17
17
|
|
18
18
|
export const LabelRow = styled.div`
|
19
19
|
height: 32px;
|
20
|
+
padding-top: 5px;
|
20
21
|
font-size: 12px;
|
21
22
|
font-weight: 500;
|
22
23
|
color: ${(props) => props.theme.semanticColors.teal[600]};
|
23
24
|
display: flex;
|
24
|
-
align-items:
|
25
|
+
align-items: center;
|
25
26
|
`
|
26
27
|
|
27
28
|
export const TotalRow = styled(LabelRow)``
|
28
29
|
|
29
30
|
export const FieldsContainer = styled.div`
|
30
31
|
flex: 1;
|
31
|
-
overflow-x: auto;
|
32
32
|
scrollbar-width: none;
|
33
33
|
|
34
34
|
&::-webkit-scrollbar {
|
@@ -63,4 +63,9 @@ export const InputGroup = styled('div', {
|
|
63
63
|
props.barWidth}px;
|
64
64
|
display: flex;
|
65
65
|
justify-content: center;
|
66
|
+
position: relative;
|
67
|
+
|
68
|
+
input[readonly] {
|
69
|
+
border: 1px solid ${(props) => props.theme.colors.grey4} !important;
|
70
|
+
}
|
66
71
|
`
|
@@ -123,11 +123,7 @@
|
|
123
123
|
return this.type === 'error_minor'
|
124
124
|
},
|
125
125
|
iconName() {
|
126
|
-
return this.type === 'warning'
|
127
|
-
? 'warning_triangle'
|
128
|
-
: this.isErrorMinor
|
129
|
-
? 'erase'
|
130
|
-
: 'info'
|
126
|
+
return this.type === 'warning' ? 'warning_triangle' : 'info'
|
131
127
|
},
|
132
128
|
presetStyles() {
|
133
129
|
// the types that doesn't have explicit border anyway have it transparent
|
@@ -146,8 +142,8 @@
|
|
146
142
|
stylesCollection.iconColor = theme.semanticColors.teal[800]
|
147
143
|
} else if (this.isErrorMinor) {
|
148
144
|
stylesCollection.borderStyle = 'dashed'
|
149
|
-
stylesCollection.borderColor = theme.colors.
|
150
|
-
stylesCollection.iconColor = theme.colors.
|
145
|
+
stylesCollection.borderColor = theme.colors.pureRed
|
146
|
+
stylesCollection.iconColor = theme.colors.pureRed
|
151
147
|
} else {
|
152
148
|
stylesCollection.borderStyle = 'dashed'
|
153
149
|
stylesCollection.borderColor = theme.colors.grey4
|
@@ -50,6 +50,7 @@
|
|
50
50
|
:has-slot="hasSlot"
|
51
51
|
:has-unit="unitName && !!unitName.length"
|
52
52
|
:input-height="inputHeight"
|
53
|
+
:is-border-error-only="isBorderErrorOnly"
|
53
54
|
:is-disabled="disabled"
|
54
55
|
:is-error="isError"
|
55
56
|
:is-interactive="isInteractive"
|
@@ -57,6 +58,7 @@
|
|
57
58
|
:no-border="noBorder"
|
58
59
|
:placeholder="displayedPlaceholder"
|
59
60
|
:read-only="isReadOnly"
|
61
|
+
:readonly="isReadOnly"
|
60
62
|
:show-arrow-controls="showArrowControls"
|
61
63
|
:show-linear-unit-name="showLinearUnitName"
|
62
64
|
:slot-size="slotSize"
|
@@ -80,7 +82,7 @@
|
|
80
82
|
>{{ unitName }}</UnitContainer
|
81
83
|
>
|
82
84
|
<IconWrapper
|
83
|
-
v-if="isError && !showLinearUnitName"
|
85
|
+
v-if="isError && !showLinearUnitName && !isBorderErrorOnly"
|
84
86
|
:margin-right="showSelect ? selectWidth : 0"
|
85
87
|
size="16px"
|
86
88
|
>
|
@@ -134,7 +136,9 @@
|
|
134
136
|
</ArrowButton>
|
135
137
|
</ArrowControls>
|
136
138
|
</InputWrapper>
|
137
|
-
<ErrorMessage v-if="isError">{{
|
139
|
+
<ErrorMessage v-if="isError && errorMessage">{{
|
140
|
+
errorMessage
|
141
|
+
}}</ErrorMessage>
|
138
142
|
</Container>
|
139
143
|
</template>
|
140
144
|
|
@@ -205,6 +209,7 @@
|
|
205
209
|
colorMode: String,
|
206
210
|
showArrowControls: Boolean,
|
207
211
|
readOnly: Boolean,
|
212
|
+
isBorderErrorOnly: Boolean,
|
208
213
|
}
|
209
214
|
|
210
215
|
const Container = styled('div', inputProps)`
|
@@ -236,16 +241,17 @@
|
|
236
241
|
showLinearUnitName,
|
237
242
|
colorMode,
|
238
243
|
showArrowControls,
|
244
|
+
isBorderErrorOnly,
|
239
245
|
}) =>
|
240
246
|
showArrowControls
|
241
247
|
? '40px'
|
242
248
|
: colorMode === 'transparent'
|
243
249
|
? '0'
|
244
250
|
: slotSize
|
245
|
-
? isError && !showLinearUnitName
|
251
|
+
? isError && !showLinearUnitName && !isBorderErrorOnly
|
246
252
|
? 'calc(' + slotSize + ' + 24px)'
|
247
253
|
: 'calc(' + slotSize + ' + 10px)'
|
248
|
-
: isError && !showLinearUnitName
|
254
|
+
: isError && !showLinearUnitName && !isBorderErrorOnly
|
249
255
|
? '24px'
|
250
256
|
: '5px'};
|
251
257
|
border-radius: ${(props) =>
|
@@ -698,6 +704,10 @@
|
|
698
704
|
type: Boolean,
|
699
705
|
default: false,
|
700
706
|
},
|
707
|
+
isBorderErrorOnly: {
|
708
|
+
type: Boolean,
|
709
|
+
default: false,
|
710
|
+
},
|
701
711
|
},
|
702
712
|
data() {
|
703
713
|
return {
|
@@ -1,8 +1,8 @@
|
|
1
1
|
<template>
|
2
2
|
<TitleWrap data-test-id="page_wrapper" :has-info-text="!!infoText">
|
3
3
|
<TitleText
|
4
|
-
data-test-id="page_title_text"
|
5
4
|
:color="color"
|
5
|
+
data-test-id="page_title_text"
|
6
6
|
:font-size="fontSize"
|
7
7
|
:uppercase="uppercase"
|
8
8
|
>
|
@@ -10,8 +10,8 @@
|
|
10
10
|
</TitleText>
|
11
11
|
<InfoText
|
12
12
|
v-if="!!infoText"
|
13
|
-
data-test-id="page_title_tooltip"
|
14
13
|
:align-arrow="infoAlign"
|
14
|
+
data-test-id="page_title_tooltip"
|
15
15
|
:text="infoText"
|
16
16
|
/>
|
17
17
|
</TitleWrap>
|
@@ -44,9 +44,8 @@
|
|
44
44
|
const titleAttrs = { color: String, fontSize: String, uppercase: Boolean }
|
45
45
|
const TitleText = styled('span', titleAttrs)`
|
46
46
|
color: ${(props) => (props.color ? props.color : props.theme.colors.black)};
|
47
|
-
font-weight:
|
47
|
+
font-weight: 500;
|
48
48
|
font-size: ${(props) => (props.fontSize ? props.fontSize : '20px')};
|
49
|
-
text-transform: ${(props) => (props.uppercase ? 'uppercase' : 'none')};
|
50
49
|
`
|
51
50
|
|
52
51
|
export default {
|