@osfarm/itineraire-technique 1.0.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,853 @@
1
+
2
+ class RotationRenderer {
3
+ constructor(divID, transcriptDivID, rotationData) {
4
+ this.barHeight = 100;
5
+
6
+ this.currentFocusIndex = null;
7
+ this.noFocusUpdate = false;
8
+
9
+ this.chart = null;
10
+
11
+ this.chartIndex = new Map();
12
+
13
+ if (Array.isArray(rotationData)) {
14
+ this.hasTimeline = true;
15
+ rotationData = rotationData.map((item) => {
16
+ return this.fixRotationData(item);
17
+ });
18
+ this.chartOptions = rotationData[0].options;
19
+ }
20
+ else {
21
+ this.hasTimeline = false;
22
+ rotationData = this.fixRotationData(rotationData);
23
+ this.chartOptions = rotationData.options;
24
+ }
25
+
26
+ this.initialLayout = this.chartOptions.view ?? 'horizontal';
27
+
28
+ this.data = rotationData;
29
+
30
+ this.divID = divID;
31
+ this.transcriptDivID = transcriptDivID;
32
+ }
33
+
34
+ fixRotationData(rotationData) {
35
+ if (rotationData.options == undefined)
36
+ rotationData.options = {};
37
+
38
+ if (rotationData.options?.view == undefined || rotationData?.options?.view == '')
39
+ rotationData.options.view = 'horizontal';
40
+
41
+ // Map rotationData items to make sure that the startDate and endDate are proper Date objects
42
+ rotationData.steps.map((item) => {
43
+ item.startDate = new Date(item.startDate);
44
+ item.endDate = new Date(item.endDate);
45
+
46
+ // Add a duration in months
47
+ item.duration = Math.round((item.endDate - item.startDate) / (30 * 1000 * 60 * 60 * 24));
48
+
49
+ return item;
50
+ });
51
+
52
+ return rotationData;
53
+ }
54
+
55
+ render() {
56
+
57
+ let self = this;
58
+
59
+ // Initialize the echarts instance based on the prepared dom
60
+ self.chart = echarts.init(document.getElementById(self.divID));
61
+
62
+ self.renderChart();
63
+
64
+ if (self.data.options.show_transcript) {
65
+ var html = this.buildHTML();
66
+ $('#' + self.transcriptDivID).html(html);
67
+ $('#' + self.transcriptDivID).show();
68
+ $('#' + self.transcriptDivID + " .rotation_item").on("click", function () {
69
+ $(this).toggleClass('show-all');
70
+ });
71
+
72
+ $('#' + self.transcriptDivID + " .rotation_item").on("mouseover", function () {
73
+ self.highlightItem(this.id);
74
+ });
75
+
76
+ // Add a click event on the transcript to scroll to the corresponding item in the chart
77
+ $('#' + self.transcriptDivID + " .intervention").on('mouseover', function (e) {
78
+ self.highlightItem(this.id);
79
+ e.stopPropagation();
80
+ });
81
+ }
82
+ else
83
+ $('#' + self.transcriptDivID).hide();
84
+
85
+ // resize all charts when the windows is resized
86
+ $(window).on('resize', _.debounce(function () {
87
+ $(".charts").each(function () {
88
+ var id = $(this).attr('_echarts_instance_');
89
+ window.echarts.getInstanceById(id).resize();
90
+ });
91
+ }, 500));
92
+ }
93
+
94
+ renderChart() {
95
+ let self = this;
96
+ let option;
97
+
98
+ this.currentFocusIndex = null;
99
+ this.noFocusUpdate = false;
100
+
101
+ if (self.initialLayout == 'horizontal')
102
+ option = this.getStepsOption();
103
+ else
104
+ option = this.getDonutOption();
105
+
106
+ self.chart.clear();
107
+ self.chart.setOption(option, false);
108
+
109
+ // console.log(option);
110
+
111
+ let options = this.chart.getOption();
112
+ options.series[0].data.forEach((item, index) => {
113
+ if (item.divId != undefined)
114
+ self.chartIndex.set(item.divId, index);
115
+ });
116
+
117
+ // Add a click event on the chart to scroll to the corresponding item in the transcript
118
+ self.chart.on('click', function (params) {
119
+ if (!params.data.divId)
120
+ return;
121
+
122
+ let element = $("#" + params.data.divId + " h4");
123
+ if (element.length == 0)
124
+ element = $("#" + params.data.divId);
125
+
126
+ self.noFocusUpdate = true;
127
+
128
+ setTimeout(() => {
129
+ self.noFocusUpdate = false;
130
+ }, 1500);
131
+
132
+ element[0].scrollIntoView({ block: "start" });
133
+
134
+ $("#" + params.data.divId).toggleClass("show-all");
135
+
136
+ self.highlightItem(params.data.divId);
137
+ });
138
+ }
139
+
140
+ highlightItem(divID) {
141
+ let self = this;
142
+
143
+ let index = self.chartIndex.get(divID);
144
+ if (index === undefined)
145
+ return;
146
+
147
+ if (self.currentFocusIndex !== null && self.currentFocusIndex != index) {
148
+ self.chart.dispatchAction({ type: 'downplay', dataIndex: self.currentFocusIndex });
149
+ }
150
+
151
+ self.currentFocusIndex = index;
152
+ self.chart.dispatchAction({ type: 'highlight', dataIndex: index });
153
+
154
+ $(".rotation_item").removeClass('highlighted');
155
+ $(".intervention").removeClass('highlighted');
156
+ $('#' + divID).addClass('highlighted');
157
+ }
158
+
159
+ getStepsOption() {
160
+ let self = this;
161
+
162
+ // Specify the configuration items and data for the chart
163
+ var categories = self.getCategoriesLabels();
164
+
165
+ let minMaxDates = {};
166
+
167
+ if (self.hasTimeline)
168
+ minMaxDates = self.getMinMaxDates(self.data[0].steps);
169
+ else
170
+ minMaxDates = self.getMinMaxDates(self.data.steps);
171
+
172
+ let option = self.getDefaultOption({
173
+ dataZoom: [
174
+ {
175
+ type: 'slider',
176
+ filterMode: 'weakFilter',
177
+ showDataShadow: false,
178
+ top: self.barHeight * 3 + 100,
179
+ labelFormatter: ''
180
+ },
181
+ {
182
+ type: 'inside',
183
+ filterMode: 'weakFilter'
184
+ }
185
+ ],
186
+
187
+ grid: {
188
+ height: self.barHeight * 3,
189
+ right: 6
190
+ },
191
+
192
+ xAxis: {
193
+ min: minMaxDates.min,
194
+ max: minMaxDates.max,
195
+ type: 'time',
196
+ axisTick: { show: true },
197
+ axisLine: { show: true },
198
+ splitLine: { show: true },
199
+ axisLabel: {
200
+ formatter: {
201
+ year: '{yyyy}',
202
+ month: '{MMM} {yy}',
203
+ day: '{d} {MMM} {yy}'
204
+ },
205
+ }
206
+ },
207
+
208
+ yAxis: {
209
+ data: categories,
210
+ axisLabel: {
211
+ width: 100,
212
+ }
213
+ },
214
+
215
+ series: []
216
+ });
217
+
218
+ if (self.hasTimeline) {
219
+ option.series = self.getStepsSeries(self.data[0].steps);
220
+ option.options = [];
221
+
222
+ self.data.forEach((item) => {
223
+ option.options.push({
224
+ series: self.getStepsSeries(item.steps)
225
+ // ,
226
+ // title: item.timelineTitle ?? item.title
227
+ });
228
+ }
229
+ );
230
+ } else {
231
+ option.series = self.getStepsSeries(self.data.steps);
232
+ }
233
+
234
+ return option;
235
+ }
236
+
237
+ getMinMaxDates(steps) {
238
+ let minDate = null;
239
+ let maxDate = null;
240
+
241
+ steps.forEach((item) => {
242
+ if (!minDate || minDate > item.startDate.valueOf())
243
+ minDate = item.startDate.valueOf();
244
+
245
+ if (!maxDate || maxDate < item.endDate.valueOf())
246
+ maxDate = item.endDate.valueOf() + 86400000 * 30; // Add some space for the end of the arrow
247
+ });
248
+
249
+ return { min: minDate, max: maxDate };
250
+ }
251
+
252
+ getStepsSeries(steps) {
253
+ let self = this;
254
+ let data = [];
255
+
256
+ steps.forEach((item, index) => {
257
+ if (item.name == Number(item.name))
258
+ item.name = "Etape " + item.name; // Force the item name to be a string
259
+
260
+ data.push({
261
+ name: item.name,
262
+ divId: 'Step_' + index,
263
+ type: 'rotation_item',
264
+ startDate: new Date(item.startDate.valueOf()), // Date de début
265
+ endDate: new Date(item.endDate.valueOf()), // Date de fin
266
+ duration: item.duration,
267
+ description: (item.description ?? ''),
268
+ value: [
269
+ 1, // Parcelle (index de la série)
270
+ item.startDate.valueOf(), // Date de début
271
+ item.endDate.valueOf(), // Date de fin
272
+ item.name, // Nom
273
+ 'rotation_item' // Type
274
+ ],
275
+ itemStyle: {
276
+ color: item.color
277
+ }
278
+ });
279
+
280
+ if (item.interventions) {
281
+ item.interventions.forEach((intervention, interventionIndex) => {
282
+
283
+ data.push({
284
+ name: intervention.name,
285
+ type: intervention.type,
286
+ value: [
287
+ intervention.type == 'intervention_top' ? 2 : 0, // Interventions en haut ou en bas (index de la série)
288
+ item.startDate.valueOf() + Number(intervention.day) * 86400000, // Date de début (ms)
289
+ item.startDate.valueOf() + (Number(intervention.day) + 1) * 86400000, // Date de début (ms)
290
+ intervention.important === true ? intervention.name + ' 🛈' : intervention.name, // Nom
291
+ intervention.type == 'intervention_top' ? 'intervention_top' : 'intervention_bottom' // Type
292
+ ],
293
+ divId: 'Intervention_' + index + '_' + interventionIndex,
294
+ interventionDate: new Date(item.startDate.valueOf()),
295
+ interventionDays: intervention.day,
296
+ itemStyle: {
297
+ color: item.color
298
+ }
299
+ });
300
+ });
301
+ }
302
+ });
303
+
304
+
305
+ let maxXPositions = new Map();
306
+
307
+ function renderItem(params, api) {
308
+
309
+ var categoryIndex = api.value(0);
310
+ var start = api.coord([api.value(1), categoryIndex]);
311
+ var end = api.coord([api.value(2), categoryIndex]);
312
+ var name = api.value(3);
313
+ var type = api.value(4);
314
+
315
+ const x = start[0];
316
+ let y = start[1];
317
+
318
+ const style = api.style();
319
+ style.opacity = 0.5;
320
+
321
+ if (params.context.rendered == undefined) {
322
+ // Start of a new rendering round
323
+ maxXPositions = new Map();
324
+
325
+ for (let catIndex = 0; catIndex < 3; catIndex++) {
326
+ for (let track = 0; track < 3; track++) {
327
+ maxXPositions.set('track_left_' + catIndex + '_' + track, params.coordSys.width);
328
+ }
329
+ }
330
+ }
331
+
332
+ params.context.rendered = true;
333
+
334
+ // Remove the default emphasis style
335
+ api.styleEmphasis({});
336
+
337
+ if (type == 'rotation_item') {
338
+
339
+ // start[0] // abscisse gauche de l'élément (après zoom)
340
+ // start[1] // ordonnée gauche de l'élément
341
+ // end[0] // abscisse droite de l'élément (après zoom)
342
+ // end[1] // ordonnée droite de l'élément
343
+ // height // Hauteur de l'élément
344
+
345
+ // params.coordSys.x // début du canva
346
+ // params.coordSys.y // début du canva
347
+ // params.coordSys.width, // largeur du canva
348
+ // params.coordSys.height // hauteur du canva
349
+
350
+ const height = self.barHeight - 40; // 20 px margin top and bottom
351
+ const arrowWidth = height / 3;
352
+ const border = 3;
353
+ const textMargin = 10;
354
+
355
+ var points = [
356
+ [x, y - height / 2],
357
+ [end[0] - border, y - height / 2],
358
+ [end[0] + arrowWidth - border, y],
359
+ [end[0] - border, y + height / 2],
360
+ [x, y + height / 2],
361
+ [x + arrowWidth, y],
362
+ ];
363
+
364
+ const itemLabelWidth = echarts.format.getTextRect(name).width + textMargin * 2;
365
+ const itemWidth = end[0] - x;
366
+
367
+ if (itemLabelWidth > itemWidth)
368
+ name = ''; // Hide the label as we won't have the room to show it
369
+
370
+ // See this for clip regions : https://stackoverflow.com/questions/71735038/setting-border-and-label-in-custom-apache-echarts
371
+ // https://stackoverflow.com/questions/73653691/how-to-draw-a-custom-triangle-in-renderitem-in-apache-echarts
372
+
373
+ return (
374
+ {
375
+ type: 'polygon',
376
+ transition: ['shape'],
377
+ shape: {
378
+ points: points
379
+ },
380
+ style: style,
381
+ emphasis: {
382
+ style: {
383
+ shadowBlur: 4,
384
+ shadowOffsetX: 1,
385
+ shadowOffsetY: 2,
386
+ shadowColor: 'rgba(0, 0, 0, 0.2)'
387
+ },
388
+ },
389
+ textConfig: {
390
+ position: [arrowWidth + textMargin, height / 2 - 5]
391
+ },
392
+ textContent: {
393
+ style: {
394
+ text: name,
395
+ fill: '#000',
396
+ width: 80,
397
+ fontWeight: 'bold'
398
+ }
399
+ }
400
+ }
401
+ );
402
+ }
403
+
404
+ if (type == 'intervention_bottom' || type == 'intervention_top') {
405
+
406
+ const height = 20;
407
+ const border = 3;
408
+ const margin = 10;
409
+ const textMargin = 5;
410
+
411
+ // Maintain a list of max x for each row. If the max x is further right than the label we try to push,
412
+ // use another row. If for all rows the space is taken, just drop this item
413
+
414
+ let trackToUse = null;
415
+ const itemLabelWidth = echarts.format.getTextRect(name).width + textMargin * 2;
416
+
417
+ for (let track = 0; track < 3; track++) {
418
+ if (!maxXPositions.has('track_right_' + categoryIndex + '_' + track)) {
419
+ // Situation where the track is empty
420
+ trackToUse = track;
421
+ break;
422
+ }
423
+
424
+ let trackLeft = maxXPositions.get('track_left_' + categoryIndex + '_' + track);
425
+ if (trackLeft > (x + itemLabelWidth)) {
426
+ // Situation where the drawing has started right of the current element
427
+ trackToUse = track;
428
+ break;
429
+ }
430
+
431
+ let trackRight = maxXPositions.get('track_right_' + categoryIndex + '_' + track);
432
+ if (trackRight < x) {
433
+ // Situation where the last painted element is sufficiently far on the left
434
+ trackToUse = track;
435
+ break;
436
+ }
437
+ }
438
+
439
+ if (trackToUse == null)
440
+ return null;
441
+
442
+ let currentRight = maxXPositions.get('track_right_' + categoryIndex + '_' + trackToUse);
443
+ if (currentRight == undefined || currentRight < x + itemLabelWidth)
444
+ maxXPositions.set('track_right_' + categoryIndex + '_' + trackToUse, x + itemLabelWidth);
445
+
446
+ let currentLeft = maxXPositions.get('track_left_' + categoryIndex + '_' + trackToUse);
447
+ if (currentLeft > x)
448
+ maxXPositions.set('track_left_' + categoryIndex + '_' + trackToUse, x);
449
+
450
+ // A nicer solution could be to draw large items in a reduced format until there is enough space for
451
+ // drawing them fully. Unfortunately that would require a two pass drawing which does not exist with Echarts ?
452
+
453
+ const arrowWidth = 3;
454
+
455
+ let arrowTop = y + 60;
456
+ let arrowBottom = y - 60;
457
+
458
+ y = margin + y + trackToUse * (height + margin) - (self.barHeight / 2);
459
+
460
+ var points = [];
461
+ var textPosition = [];
462
+
463
+ if (type == 'intervention_top') {
464
+ textPosition = [textMargin, textMargin];
465
+ points = [
466
+ [x, y],
467
+ [x + itemLabelWidth, y],
468
+ [x + itemLabelWidth, y + height],
469
+ [x + arrowWidth, y + height],
470
+ [x, arrowTop]
471
+ ];
472
+ }
473
+ else {
474
+ textPosition = [textMargin, textMargin + y - arrowBottom];
475
+ points = [
476
+ [x, arrowBottom],
477
+ [x + arrowWidth, y],
478
+ [x + itemLabelWidth, y],
479
+ [x + itemLabelWidth, y + height],
480
+ [x, y + height]
481
+ ];
482
+ }
483
+
484
+ return ({
485
+ type: 'polygon',
486
+ transition: ['shape'],
487
+ shape: {
488
+ points: points
489
+ },
490
+ style: api.style({
491
+ fill: style.fill,
492
+ stroke: style.fill,
493
+ textFill: '#000',
494
+ }),
495
+ emphasis: {
496
+ style: {
497
+ shadowBlur: 4,
498
+ shadowOffsetX: 1,
499
+ shadowOffsetY: 2,
500
+ shadowColor: 'rgba(0, 0, 0, 0.2)'
501
+ },
502
+ },
503
+ textConfig: {
504
+ position: textPosition
505
+ },
506
+ textContent: {
507
+ style: {
508
+ text: name,
509
+ fill: '#000',
510
+ }
511
+ }
512
+ }
513
+ );
514
+
515
+ }
516
+ }
517
+
518
+ return [
519
+ {
520
+ type: 'custom',
521
+ renderItem: renderItem,
522
+ clip: true,
523
+ itemStyle: {
524
+ opacity: 0.8
525
+ },
526
+ encode: {
527
+ x: [1, 2],
528
+ y: 0
529
+ },
530
+ data: data
531
+ }
532
+ ];
533
+ }
534
+
535
+ getCategoriesLabels() {
536
+ let self = this;
537
+
538
+ let categories = [self.chartOptions.title_bottom_interventions ?? '',
539
+ self.chartOptions.title_steps ?? '',
540
+ self.chartOptions.title_top_interventions ?? ''];
541
+
542
+ // simulate some wrapping of the category labels
543
+ categories = categories.map((item) => {
544
+ return Array.from(item.matchAll(/(?=\S).{0,13}\S(?!\S)|\S{7}/gm), (m) => m[0]).join("\n");
545
+ });
546
+
547
+ return categories;
548
+ }
549
+
550
+ buildHTML() {
551
+ let html = '';
552
+
553
+ let self = this;
554
+
555
+ self.data.steps.forEach((item, index) => {
556
+
557
+ let collapseButton = '';
558
+ if (item.interventions?.length > 0 || item.attributes?.length > 0)
559
+ collapseButton = '<div class="collapse-button"><i class="fa fa-chevron-down" aria-hidden="true"></i></div>';
560
+
561
+ let start = item.startDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: '2-digit' });
562
+ let end = item.endDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: '2-digit' });
563
+
564
+ let dates = '<b>' + item.duration + ' mois</b> (' + start + ' ➜ ' + end + ')';
565
+
566
+ html += '<div id="Step_' + index + '" class="rotation_item" style="border-color: ' + item.color + '">'
567
+ + '<div class="step_dates">' + dates + '</div>'
568
+ + '<h4>' + item.name + '<i class="fa fa-pencil step-edit" aria-hidden="true"></i></h4>'
569
+ + collapseButton
570
+ + '<p class="step_description">' + (item.description ?? '') + '</p>'
571
+ + '<div class="details">'
572
+ + (item.attributes?.length > 0 ? item.attributes.map((attribute) => { return '<p><dt>' + attribute.name + '</dt><dd>' + attribute.value + '</dd></p>' }).join('') : '');
573
+
574
+ if (item.interventions?.length > 0) {
575
+ html += '<h5>Interventions</h5>';
576
+ item.interventions.forEach((intervention, interventionIndex) => {
577
+ let intDate = new Date(item.startDate.valueOf() + intervention.day * 86400000).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
578
+ let days = intervention.day;
579
+ if (days >= 0)
580
+ intDate += ' (J+' + days + ')';
581
+ else
582
+ intDate += ' (J' + days + ')';
583
+
584
+ let title = intervention.name;
585
+
586
+ if (intervention.important === true)
587
+ title = '<i class="fa fa-exclamation-circle" aria-hidden="true" style="color: #ff9a1c"></i> ' + title;
588
+
589
+ html += '<div id="Intervention_' + index + '_' + interventionIndex + '" class="intervention"><span class="intervention_title">' + title + '</span>'
590
+ + '<span class="intervention_date badge rounded-pill">' + intDate + '</span>'
591
+ + '<div class="intervention_description">' + intervention.description + '</div></div>';
592
+ });
593
+ }
594
+
595
+ html += '</div></div>';
596
+ });
597
+
598
+ return '<div>' + html + '</div>';
599
+ }
600
+
601
+ getDonutOption() {
602
+ let self = this;
603
+
604
+ let option = self.getDefaultOption({
605
+ });
606
+
607
+ if (self.hasTimeline) {
608
+ option.series = this.getDonutSeries(self.data[0].steps);
609
+ option.options = [];
610
+ self.data.forEach((item) => {
611
+ option.options.series = this.getDonutSeries(item.steps);
612
+ });
613
+ }
614
+ else {
615
+ option.series = this.getDonutSeries(self.data.steps);
616
+ }
617
+
618
+ return option;
619
+ }
620
+
621
+ getDonutSeries(steps) {
622
+ let series = [];
623
+ let self = this;
624
+
625
+ // Build the crop ring
626
+ let crops = {
627
+ name: 'Rotation',
628
+ type: 'pie',
629
+ top: '40',
630
+ radius: ['70%', '100%'],
631
+ labelLine: {
632
+ length: 30
633
+ },
634
+ label: {
635
+ position: 'inner',
636
+ fontWeight: 'bold'
637
+ },
638
+ data: []
639
+ };
640
+
641
+ let totalMonths = 0;
642
+ let minDate = null;
643
+ let lastDayOfPreviousStep = null;
644
+
645
+ steps.forEach((item, index) => {
646
+
647
+ totalMonths += item.duration;
648
+
649
+ if (!lastDayOfPreviousStep)
650
+ lastDayOfPreviousStep = new Date(item.startDate.valueOf());
651
+
652
+ let days = Math.round((item.endDate - lastDayOfPreviousStep) / (1000 * 60 * 60 * 24));
653
+ lastDayOfPreviousStep = new Date(item.endDate.valueOf());
654
+
655
+ if (!minDate)
656
+ minDate = new Date(item.startDate.valueOf());
657
+
658
+ let pieItem = {
659
+ 'name': item.name,
660
+ 'value': days,
661
+ 'divId': 'Step_' + index,
662
+ 'type': 'rotation_item',
663
+ 'startDate': new Date(item.startDate.valueOf()), // Date de début
664
+ 'endDate': new Date(item.endDate.valueOf()), // Date de fin
665
+ 'duration': item.duration,
666
+ 'description': (item.description ?? '')// + (item.attributes ? item.attributes.map((attribute) => { return '<p><dt>' + attribute.name + '</dt><dd>' + attribute.value + '</dd></p>' }).join('') : '')
667
+ };
668
+
669
+ if (pieItem.color != '#ffffff')
670
+ pieItem.itemStyle = { 'color': item.color };
671
+
672
+ crops.data.push(pieItem);
673
+ });
674
+
675
+ series.push(crops);
676
+
677
+ // Create the calendar ring
678
+ let months = {
679
+ name: 'Months',
680
+ type: 'pie',
681
+ top: '40',
682
+ radius: ['60%', '70%'],
683
+ label: {
684
+ position: 'inner',
685
+ rotate: 'tangential'
686
+ },
687
+ tooltip: {
688
+ show: true,
689
+ formatter: '{b}'
690
+ },
691
+ itemStyle: {
692
+ borderColor: '#555',
693
+ color: '#FFFFFF',
694
+ borderWidth: 0
695
+ },
696
+ emphasis: { disabled: true },
697
+ data: []
698
+ };
699
+
700
+ const monthsColorScale = [
701
+ '#c7d2e3', // winter
702
+ '#c7d2e3',
703
+ '#bdd8c0', // spring
704
+ '#bdd8c0',
705
+ '#bdd8c0',
706
+ '#ecebb3', // summer
707
+ '#ecebb3',
708
+ '#ecebb3',
709
+ '#f8e0c5', // automn
710
+ '#f8e0c5',
711
+ '#f8e0c5',
712
+ '#c7d2e3'
713
+ ];
714
+
715
+ let monthsPerYear = new Map();
716
+ for (let month = 1; month <= totalMonths; month++) {
717
+ let monthName = minDate.toLocaleDateString(undefined, { month: 'short' });
718
+
719
+ let item = { 'name': monthName, 'value': 1 };
720
+ const year = minDate.getFullYear();
721
+
722
+ item.itemStyle = { color: monthsColorScale[minDate.getMonth()] };
723
+
724
+ months.data.push(item);
725
+
726
+ let currentMonthsPerYear = monthsPerYear.get(year);
727
+ if (currentMonthsPerYear == undefined)
728
+ currentMonthsPerYear = 0;
729
+ monthsPerYear.set(year, ++currentMonthsPerYear);
730
+
731
+ // increment the current month
732
+ minDate.setMonth(minDate.getMonth() + 1);
733
+ }
734
+
735
+ series.push(months);
736
+
737
+ // Create the calendar years ring
738
+ let years = {
739
+ name: 'Years',
740
+ type: 'pie',
741
+ top: '40',
742
+ radius: ['45%', '60%'],
743
+ label: {
744
+ position: 'inner',
745
+ rotate: 'tangential',
746
+ fontWeight: 'bold'
747
+ },
748
+ emphasis: { disabled: true },
749
+ tooltip: { show: false },
750
+ itemStyle: {
751
+ color: '#FFFFFF',
752
+ borderWidth: 1
753
+ },
754
+ data: []
755
+ };
756
+
757
+ monthsPerYear.forEach((nbMonths, year) => {
758
+ years.data.push({ 'name': year, 'value': nbMonths });
759
+ });
760
+
761
+ series.push(years);
762
+
763
+ return series;
764
+ }
765
+
766
+ getDefaultOption(option) {
767
+
768
+ let self = this;
769
+
770
+ option.title = {
771
+ text: self.data.title,
772
+ left: 'center'
773
+ };
774
+
775
+ if (Array.isArray(self.data) && self.hasTimeline) {
776
+ option.timeline = {
777
+ axisType: 'category',
778
+ // realtime: false,
779
+ // loop: false,
780
+ autoPlay: true,
781
+ top: self.barHeight * 3 + 150,
782
+ // currentIndex: 2,
783
+ playInterval: 5000,
784
+ replaceMerge: 'series',
785
+
786
+ // controlStyle: {
787
+ // position: 'left'
788
+ // },
789
+ data: [],
790
+ };
791
+
792
+ self.data.forEach((item) => {
793
+ option.timeline.data.push(item.timelineTitle ?? item.title);
794
+ });
795
+ }
796
+
797
+ option.tooltip = {
798
+ formatter: function (params) {
799
+ if (params.data.type == 'rotation_item') {
800
+ let start = params.data.startDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: '2-digit' });
801
+ let end = params.data.endDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: '2-digit' });
802
+
803
+ return params.marker + params.name + ' : ' + params.data.duration + ' mois (' + start + ' ➜ ' + end + ')<br>' + params.data.description;
804
+ }
805
+ else {
806
+ let interventionDate = params.data.interventionDate;
807
+ const days = params.data.interventionDays;
808
+ let dateString = interventionDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
809
+
810
+ if (days >= 0)
811
+ dateString += ' (J+' + days + ')';
812
+ else
813
+ dateString += ' (J' + days + ')';
814
+
815
+ return params.marker + params.name + ' - ' + dateString;
816
+ }
817
+ }
818
+ };
819
+
820
+ option.toolbox = {
821
+ "itemSize": 25,
822
+ "iconStyle": {
823
+ "borderColor": "#AAA",
824
+ "borderWidth": 1
825
+ },
826
+ "feature": {
827
+ "myTool1": {
828
+ "show": true,
829
+ "title": 'Rotation',
830
+ "icon": 'path://M18.15,12.99c-.06-.07-.14-.13-.23-.17l-.38-.15c.33-.93.51-1.92.51-2.96,0-3-1.49-5.65-3.77-7.26l-3.03,2.72c1.72.79,2.91,2.53,2.91,4.54,0,.54-.09,1.07-.25,1.56l-.44-.17c-.3-.12-.64.03-.76.33-.04.1-.05.21-.03.31l.79,4.82c.06.32.36.53.68.47.1-.02.19-.06.27-.13l3.66-3.1c.25-.21.28-.58.07-.82ZM6.94,5.24c.46-.23.97-.39,1.5-.47l.13.59c.07.32.38.52.69.45.1-.02.2-.07.28-.14l3.59-3.31c.23-.22.24-.59.02-.83-.07-.07-.16-.13-.26-.16L8.3.02c-.31-.09-.63.09-.73.39-.03.09-.03.19-.01.29l.06.27c-1.12.2-2.16.6-3.1,1.17l2.41,3.09ZM5.88,5.96l-2.39-3.06c-.98.82-1.79,1.84-2.34,3.01l3.62,1.44c.29-.53.66-1,1.11-1.39ZM4.17,9.72c0-.41.05-.81.14-1.19l-3.63-1.44c-.18.57-.3,1.16-.35,1.77l3.84,1.1c0-.08,0-.16,0-.24ZM9.17,14.72c-1.59,0-3-.74-3.92-1.9l.42-.38c.24-.22.26-.59.04-.83-.07-.08-.16-.14-.26-.17l-4.66-1.47c-.31-.09-.64.08-.73.39-.03.1-.03.2,0,.3l1.12,4.66c.07.31.39.51.7.43.09-.02.18-.07.25-.13l.23-.21c1.63,1.94,4.07,3.18,6.8,3.18,1.3,0,2.54-.28,3.66-.79l-.8-3.99c-.81.56-1.79.9-2.86.9Z',
831
+ onclick: function (){
832
+ self.initialLayout = 'donut';
833
+ self.renderChart();
834
+ }
835
+ },
836
+ "myTool2": {
837
+ "show": true,
838
+ "title": 'Frise',
839
+ "icon": 'path://M4.63,0H0v10.01h4.97c.07,0,.13-.05.19-.14l2.61-4.14c.17-.28.23-.89.12-1.35-.03-.15-.08-.27-.13-.35L5.16.12c-.05-.08-.11-.12-.17-.12h-.37ZM11.9,4.38c-.03-.15-.08-.27-.13-.35L9.17.12c-.05-.08-.11-.12-.17-.12h-.37s-2.26,0-2.26,0v.03s.06.05.08.09l2.6,3.9c.06.08.1.21.13.35.1.47.05,1.07-.12,1.35l-2.61,4.14s-.05.07-.08.09v.04h2.6c.07,0,.13-.05.19-.14l2.61-4.14c.17-.28.23-.89.12-1.35ZM18.28,4.38c-.03-.15-.08-.27-.13-.35L15.55.12c-.05-.08-.11-.12-.17-.12h-.37s-4.63,0-4.63,0v.03s.06.05.08.09l2.6,3.9c.06.08.1.21.13.35.1.47.05,1.07-.12,1.35l-2.61,4.14s-.05.07-.08.09v.04h4.97c.07,0,.13-.05.19-.14l2.61-4.14c.17-.28.23-.89.12-1.35Z',
840
+ onclick: function (){
841
+ self.initialLayout = 'horizontal';
842
+ self.renderChart();
843
+ }
844
+ },
845
+ "saveAsImage": {
846
+ 'excludeComponents': ["dataZoom", "toolbox"]
847
+ }
848
+ }
849
+ };
850
+
851
+ return option;
852
+ }
853
+ }