1
+ <script setup>
2
+ import { ref , computed , nextTick , onMounted } from " vue" ;
3
+ import pdf from " ../pdf" ;
4
+ import img from " ../img" ;
5
+ import { useNestedProp } from " ../useNestedProp" ;
6
+ import mainConfig from " ../default_configs.json" ;
7
+ import { shiftHue } from " ../lib" ;
8
+ import Title from " ../atoms/Title.vue" ;
9
+ import UserOptions from " ../atoms/UserOptions.vue" ;
10
+
11
+ const props = defineProps ({
12
+ config: {
13
+ type: Object ,
14
+ default () {
15
+ return {}
16
+ }
17
+ },
18
+ dataset: {
19
+ type: Object ,
20
+ default () {
21
+ return {}
22
+ }
23
+ },
24
+ });
25
+
26
+ const uid = ref (` vue-ui-tiremarks-${ Math .random ()} ` );
27
+
28
+ const defaultConfig = ref (mainConfig .vue_ui_tiremarks );
29
+
30
+ const isPrinting = ref (false );
31
+ const isImaging = ref (false );
32
+ const tiremarksChart = ref (null );
33
+
34
+ const tiremarksConfig = computed (() => {
35
+ return useNestedProp ({
36
+ userConfig: props .config ,
37
+ defaultConfig: defaultConfig .value
38
+ });
39
+ });
40
+
41
+ const activeValue = ref (tiremarksConfig .value .style .chart .animation .use ? 0 : props .dataset .percentage );
42
+
43
+ onMounted (() => {
44
+ let acceleration = 0 ;
45
+ let speed = tiremarksConfig .value .style .chart .animation .speed ;
46
+ let incr = (0.005 ) * tiremarksConfig .value .style .chart .animation .acceleration ;
47
+ function animate () {
48
+ activeValue .value += speed + acceleration;
49
+ acceleration += incr;
50
+ if (activeValue .value < props .dataset .percentage ) {
51
+ requestAnimationFrame (animate);
52
+ } else {
53
+ activeValue .value = props .dataset .percentage ;
54
+ }
55
+ }
56
+
57
+ if (tiremarksConfig .value .style .chart .animation .use ) {
58
+ activeValue .value = 0 ;
59
+ animate ();
60
+ }
61
+ });
62
+
63
+ const isVertical = computed (() => {
64
+ return tiremarksConfig .value .style .chart .layout .display === ' vertical' ;
65
+ });
66
+
67
+ const hasGradient = computed (() => {
68
+ return tiremarksConfig .value .style .chart .layout .ticks .gradient .show ;
69
+ });
70
+
71
+ const padding = computed (() => {
72
+
73
+ const paddingRef = {
74
+ top: 48 ,
75
+ left: 64 ,
76
+ right: 64 ,
77
+ bottom: 48
78
+ }
79
+
80
+ if (isVertical .value ) {
81
+ return {
82
+ top: tiremarksConfig .value .style .chart .percentage .verticalPosition === ' top' ? paddingRef .top : 3 ,
83
+ left: 3 ,
84
+ right: 3 ,
85
+ bottom: tiremarksConfig .value .style .chart .percentage .verticalPosition === ' bottom' ? paddingRef .bottom : 3
86
+ }
87
+ } else {
88
+ return {
89
+ top: 0 ,
90
+ bottom: 0 ,
91
+ left: tiremarksConfig .value .style .chart .percentage .horizontalPosition === ' left' ? paddingRef .left : 3 ,
92
+ right: tiremarksConfig .value .style .chart .percentage .horizontalPosition === ' right' ? paddingRef .right : 10 ,
93
+ }
94
+ }
95
+ })
96
+
97
+
98
+ // This should return a total for x and another for y
99
+ const totalPadding = computed (() => {
100
+ return Object .values (padding .value ).reduce ((a , b ) => a + b, 0 )
101
+ })
102
+
103
+ const svg = computed (() => {
104
+ const small = 56 ;
105
+ const big = 312 ;
106
+ return {
107
+ height: isVertical .value ? big : small,
108
+ width: isVertical .value ? small : big,
109
+ }
110
+ })
111
+
112
+ const max = ref (100 );
113
+
114
+ const proportion = computed (() => {
115
+ return props .dataset .percentage / max .value
116
+ });
117
+
118
+ const tickSize = computed (() => {
119
+ if (isVertical .value ) {
120
+ return {
121
+ mark: ((svg .value .height - totalPadding .value ) / 100 ) * 0.5 ,
122
+ space: ((svg .value .height - totalPadding .value ) / 100 ) * 0.5
123
+ }
124
+ } else {
125
+ return {
126
+ mark: ((svg .value .width - totalPadding .value ) / 100 ) * 0.5 ,
127
+ space: ((svg .value .width - totalPadding .value ) / 100 ) * 0.5
128
+ }
129
+ }
130
+ })
131
+
132
+ const ticks = computed (() => {
133
+ const arr = [];
134
+ const marks = 100 ;
135
+ for (let i = 0 ; i < marks; i += 1 ) {
136
+ const color = tiremarksConfig .value .style .chart .layout .ticks .gradient .show ? shiftHue (tiremarksConfig .value .style .chart .layout .activeColor , i / marks * (tiremarksConfig .value .style .chart .layout .ticks .gradient .shiftHueIntensity / 100 )) : tiremarksConfig .value .style .chart .layout .activeColor ;
137
+ if (isVertical .value ) {
138
+ const verticalCrescendo = tiremarksConfig .value .style .chart .layout .crescendo ? ((marks - i) * (svg .value .width - padding .value .left - padding .value .right ) / marks / 3 ) : 0 ;
139
+ const v_x1 = padding .value .left + 4 + verticalCrescendo;
140
+ const v_x2 = svg .value .width - padding .value .right - 4 - verticalCrescendo;
141
+ const v_y1 = svg .value .height - padding .value .bottom - (i * tickSize .value .mark ) - (i * tickSize .value .space ) - tickSize .value .mark ;
142
+ const v_y2 = svg .value .height - padding .value .bottom - (i * tickSize .value .mark ) - (i * tickSize .value .space ) - tickSize .value .mark ;
143
+ const v_space_x = (v_x2 - v_x1 ) / tiremarksConfig .value .style .chart .layout .curveAngleX ;
144
+ const v_space_y = tiremarksConfig .value .style .chart .layout .curveAngleY * ((1 + i) / marks);
145
+ arr .push ({
146
+ x1: v_x1,
147
+ x2: v_x2,
148
+ y1: v_y1,
149
+ y2: v_y2,
150
+ curve: ` M ${ v_x1} ${ v_y1} C ${ v_x1 + v_space_x} ${ v_y1 - v_space_y} , ${ v_x2 - v_space_x} ${ v_y2 - v_space_y} , ${ v_x2} ${ v_y2} ` ,
151
+ color
152
+ })
153
+ } else {
154
+ const horizontalCrescendo = tiremarksConfig .value .style .chart .layout .crescendo ? ((marks - i) * (svg .value .height - padding .value .top - padding .value .bottom ) / marks / 3 ) : 0 ;
155
+ const h_x1 = padding .value .left + (i * tickSize .value .mark ) + (i * tickSize .value .space ) - tickSize .value .mark ;
156
+ const h_x2 = h_x1;
157
+ const h_y1 = padding .value .top + 4 + horizontalCrescendo;
158
+ const h_y2 = svg .value .height - padding .value .bottom - 4 - horizontalCrescendo;
159
+ const h_space_x = tiremarksConfig .value .style .chart .layout .curveAngleY * ((1 + i) / marks);
160
+ const h_space_y = (h_y2 - h_y1 ) / tiremarksConfig .value .style .chart .layout .curveAngleX ;
161
+ arr .push ({
162
+ x1: h_x1,
163
+ x2: h_x2,
164
+ y1: h_y1,
165
+ y2: h_y2,
166
+ curve: ` M ${ h_x1} ${ h_y1} C ${ h_x1 + h_space_x} ${ h_y1 + h_space_y} , ${ h_x2 + h_space_x} ${ h_y2 - h_space_y} , ${ h_x2} ${ h_y2} ` ,
167
+ color
168
+ })
169
+ }
170
+ }
171
+ return arr;
172
+ });
173
+
174
+ const dataLabel = computed (() => {
175
+ let x,y,textAnchor,fontSize;
176
+ const fontSizeOffset = tiremarksConfig .value .style .chart .percentage .fontSize / 3 ;
177
+
178
+ if (isVertical .value ) {
179
+ if (tiremarksConfig .value .style .chart .percentage .verticalPosition === ' top' ) {
180
+ x = svg .value .width / 2 ;
181
+ y = padding .value .top / 2 ;
182
+ textAnchor = ' middle' ;
183
+ } else if (tiremarksConfig .value .style .chart .percentage .verticalPosition === ' bottom' ) {
184
+ x = svg .value .width / 2 ;
185
+ y = svg .value .height - (padding .value .bottom / 2 ) + fontSizeOffset;
186
+ textAnchor = ' middle' ;
187
+ }
188
+ } else {
189
+ if (tiremarksConfig .value .style .chart .percentage .horizontalPosition === ' left' ) {
190
+ x = 4 ;
191
+ y = (svg .value .height / 2 ) + fontSizeOffset;
192
+ textAnchor = ' start' ;
193
+ } else if (tiremarksConfig .value .style .chart .percentage .horizontalPosition === ' right' ) {
194
+ x = svg .value .width - padding .value .right + 8 ;
195
+ y = (svg .value .height / 2 ) + fontSizeOffset;
196
+ textAnchor = ' start' ;
197
+ }
198
+ }
199
+
200
+ return {
201
+ x,
202
+ y,
203
+ textAnchor,
204
+ bold: tiremarksConfig .value .style .chart .percentage .bold ,
205
+ fontSize: tiremarksConfig .value .style .chart .percentage .fontSize ,
206
+ fill: tiremarksConfig .value .style .chart .percentage .color
207
+ }
208
+ })
209
+
210
+ const __to__ = ref (null );
211
+
212
+ function showSpinnerPdf () {
213
+ isPrinting .value = true ;
214
+ }
215
+
216
+ function generatePdf (){
217
+ showSpinnerPdf ();
218
+ clearTimeout (__to__ .value );
219
+ __to__ .value = setTimeout (() => {
220
+ pdf ({
221
+ domElement: document .getElementById (` ${ uid .value } ` ),
222
+ fileName: tiremarksConfig .value .style .chart .title .text || ' vue-ui-tiremarks'
223
+ }).finally (() => {
224
+ isPrinting .value = false ;
225
+ })
226
+ }, 100 )
227
+ }
228
+
229
+ function showSpinnerImage () {
230
+ isImaging .value = true ;
231
+ }
232
+
233
+ function generateImage () {
234
+ showSpinnerImage ();
235
+ clearTimeout (__to__ .value );
236
+ __to__ .value = setTimeout (() => {
237
+ img ({
238
+ domElement: document .getElementById (` ${ uid .value } ` ),
239
+ fileName: tiremarksConfig .value .style .chart .title .text || ' vue-ui-tiremarks' ,
240
+ format: ' png'
241
+ }).finally (() => {
242
+ isImaging .value = false ;
243
+ })
244
+ }, 100 )
245
+ }
246
+
247
+ defineExpose ({
248
+ generatePdf,
249
+ generateImage
250
+ });
251
+
252
+ </script >
253
+
254
+ <template >
255
+ <div :ref =" `tiremarksChart`" :class =" `vue-ui-tiremarks ${tiremarksConfig.useCssAnimation ? '' : 'vue-ui-dna'}`" :style =" `font-family:${tiremarksConfig.style.fontFamily};width:100%; text-align:center;${(tiremarksConfig.userOptions.show && !isImaging) ? 'padding-top:36px' : ''}`" :id =" uid" >
256
+
257
+ <div v-if =" tiremarksConfig.style.chart.title.text" :style =" `width:100%;background:${tiremarksConfig.style.chart.backgroundColor};padding-bottom:12px`" >
258
+ <Title
259
+ :config =" {
260
+ title: {
261
+ cy: 'wheel-title',
262
+ text: tiremarksConfig.style.chart.title.text,
263
+ color: tiremarksConfig.style.chart.title.color,
264
+ fontSize: tiremarksConfig.style.chart.title.fontSize,
265
+ bold: tiremarksConfig.style.chart.title.bold
266
+ },
267
+ subtitle: {
268
+ cy: 'wheel-subtitle',
269
+ text: tiremarksConfig.style.chart.title.subtitle.text,
270
+ color: tiremarksConfig.style.chart.title.subtitle.color,
271
+ fontSize: tiremarksConfig.style.chart.title.subtitle.fontSize,
272
+ bold: tiremarksConfig.style.chart.title.subtitle.bold
273
+ },
274
+ }"
275
+ />
276
+ </div >
277
+
278
+ <UserOptions
279
+ ref =" details"
280
+ v-if =" tiremarksConfig.userOptions.show"
281
+ :backgroundColor =" tiremarksConfig.style.chart.backgroundColor"
282
+ :color =" tiremarksConfig.style.chart.color"
283
+ :isPrinting =" isPrinting"
284
+ :isImaging =" isImaging"
285
+ :title =" tiremarksConfig.userOptions.title"
286
+ :uid =" uid"
287
+ :hasImg =" true"
288
+ :hasXls =" false"
289
+ @generatePdf =" generatePdf"
290
+ @generateImage =" generateImage"
291
+ />
292
+
293
+ <svg :viewBox =" `0 0 ${svg.width} ${svg.height}`" :style =" `max-width:100%; overflow: visible; background:${tiremarksConfig.style.chart.backgroundColor};color:${tiremarksConfig.style.chart.color}`" >
294
+ <g v-if =" tiremarksConfig.style.chart.layout.curved" >
295
+ <path
296
+ v-for =" (tick, i) in ticks"
297
+ :d =" tick.curve"
298
+ :stroke-width =" tickSize.mark"
299
+ :stroke =" activeValue >= i ? tick.color : tiremarksConfig.style.chart.layout.inactiveColor"
300
+ stroke-linecap =" round"
301
+ fill =" none"
302
+ :class =" { 'vue-ui-tick-animated': tiremarksConfig.style.chart.animation.use && i <= activeValue }"
303
+ />
304
+ </g >
305
+ <g v-else >
306
+ <line
307
+ v-for =" (tick, i) in ticks"
308
+ :x1 =" tick.x1"
309
+ :y1 =" tick.y1"
310
+ :x2 =" tick.x2"
311
+ :y2 =" tick.y2"
312
+ :stroke-width =" tickSize.mark"
313
+ :stroke =" activeValue >= i ? tick.color : tiremarksConfig.style.chart.layout.inactiveColor"
314
+ stroke-linecap =" round"
315
+ />
316
+ </g >
317
+ <text
318
+ v-if =" tiremarksConfig.style.chart.percentage.show"
319
+ :x =" dataLabel.x"
320
+ :y =" dataLabel.y"
321
+ :font-size =" dataLabel.fontSize"
322
+ :fill =" tiremarksConfig.style.chart.layout.ticks.gradient.show && tiremarksConfig.style.chart.percentage.useGradientColor ? shiftHue(tiremarksConfig.style.chart.layout.activeColor, activeValue / 100 * (tiremarksConfig.style.chart.layout.ticks.gradient.shiftHueIntensity / 100)) : tiremarksConfig.style.chart.percentage.color"
323
+ :font-weight =" dataLabel.bold ? 'bold': 'normal'"
324
+ :text-anchor =" dataLabel.textAnchor"
325
+ >
326
+ {{ activeValue.toFixed(tiremarksConfig.style.chart.percentage.rounding) + '%' }}
327
+ </text >
328
+ </svg >
329
+ </div >
330
+ </template >
331
+
332
+ <style scoped>
333
+ .vue-ui-tiremarks * {
334
+ transition : unset ;
335
+ }
336
+ .vue-ui-tiremarks {
337
+ user-select : none ;
338
+ position : relative ;
339
+ }
340
+ .vue-ui-tick-animated {
341
+ animation : animate-tick 0.3s ease-in ;
342
+ transform-origin : center ;
343
+ }
344
+
345
+ @keyframes animate-tick {
346
+ 0% {
347
+ stroke-width : 2 ;
348
+ transform : scale (1 ,1.1 );
349
+ }
350
+ 100% {
351
+ stroke-width : initial ;
352
+ transform : scale (1 ,1 );
353
+ }
354
+ }
355
+ </style >
0 commit comments