@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.
- package/.github/workflows/publish.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/css/styles-editor.css +1 -0
- package/css/styles-editor.css.map +1 -0
- package/css/styles-rendering.css +1 -0
- package/css/styles-rendering.css.map +1 -0
- package/editor.html +725 -0
- package/images/rendu_frise.png +0 -0
- package/images/rendu_rotation.png +0 -0
- package/js/chart-render.js +853 -0
- package/js/editor-attributes.js +96 -0
- package/js/editor-crops.js +78 -0
- package/js/editor-export.js +118 -0
- package/js/editor-interventions.js +117 -0
- package/package.json +16 -0
- package/rendu_statique_1.html +664 -0
- package/scss/styles-editor.scss +106 -0
- package/scss/styles-rendering.scss +101 -0
- package/test/itk-templates/export-itk-1.json +153 -0
- package/test/test.json +324 -0
|
@@ -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
|
+
}
|