@osimatic/helpers-js 1.2.8 → 1.3.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/chartjs.js +309 -0
- package/date_time.js +52 -15
- package/duration.js +1 -2
- package/index.js +4 -3
- package/package.json +1 -1
package/chartjs.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
class Chartjs {
|
|
2
|
+
static init() {
|
|
3
|
+
if (typeof Chartjs.initialized == 'undefined' || Chartjs.initialized) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
Chartjs.initialized = true;
|
|
8
|
+
|
|
9
|
+
const centerTextPlugin = {
|
|
10
|
+
id: 'centerText',
|
|
11
|
+
afterDraw(chart) {
|
|
12
|
+
if (typeof chart.config.options.centerText == 'undefined' || !chart.config.options.centerText.display) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { ctx, chartArea: { width, height } } = chart;
|
|
17
|
+
ctx.save();
|
|
18
|
+
const total = chart.data.datasets[0].data.reduce((a,b) => a+b, 0);
|
|
19
|
+
const label = chart.config.options.centerText.label || '';
|
|
20
|
+
const fontSize = chart.config.options.centerText.fontSize || 16;
|
|
21
|
+
const textColor = chart.config.options.centerText.color || '#333';
|
|
22
|
+
|
|
23
|
+
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
24
|
+
ctx.fillStyle = textColor;
|
|
25
|
+
ctx.textAlign = 'center';
|
|
26
|
+
ctx.textBaseline = 'middle';
|
|
27
|
+
ctx.fillText(`${total}`, width / 2, height / 2);
|
|
28
|
+
if (label) {
|
|
29
|
+
ctx.font = `12px sans-serif`;
|
|
30
|
+
ctx.fillText(label, width / 2, height / 2 + 20);
|
|
31
|
+
}
|
|
32
|
+
ctx.restore();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Chart.register(centerTextPlugin);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static createStackedChart(chartDiv, chartData, title=null, options={}) {
|
|
40
|
+
chartDiv.empty();
|
|
41
|
+
new Chart(chartDiv.get(0).getContext("2d"), $.extend(true, {}, {
|
|
42
|
+
type: "bar",
|
|
43
|
+
data: {
|
|
44
|
+
labels: chartData.labels,
|
|
45
|
+
datasets: chartData.datasets
|
|
46
|
+
},
|
|
47
|
+
options: {
|
|
48
|
+
responsive: true,
|
|
49
|
+
scales: {
|
|
50
|
+
x: {
|
|
51
|
+
stacked: true
|
|
52
|
+
},
|
|
53
|
+
y: {
|
|
54
|
+
stacked: true,
|
|
55
|
+
beginAtZero: true,
|
|
56
|
+
ticks: {
|
|
57
|
+
precision: 0
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
plugins: {
|
|
62
|
+
title: {
|
|
63
|
+
display: null !== title,
|
|
64
|
+
text: title || '',
|
|
65
|
+
font: { size: 14 },
|
|
66
|
+
color: "#333",
|
|
67
|
+
padding: { top: 10, bottom: 20 }
|
|
68
|
+
},
|
|
69
|
+
legend: {
|
|
70
|
+
position: "bottom",
|
|
71
|
+
labels: {
|
|
72
|
+
usePointStyle: true,
|
|
73
|
+
padding: 15
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
tooltip: {
|
|
77
|
+
callbacks: {
|
|
78
|
+
label: function(context) {
|
|
79
|
+
return `${context.dataset.label}: ${context.parsed.y}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, options));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static createBarChart(chartDiv, chartData, title=null, options={}) {
|
|
89
|
+
chartDiv.empty();
|
|
90
|
+
new Chart(chartDiv.get(0).getContext("2d"), $.extend(true, {}, {
|
|
91
|
+
type: "bar",
|
|
92
|
+
data: {
|
|
93
|
+
labels: chartData.labels,
|
|
94
|
+
datasets: chartData.datasets
|
|
95
|
+
},
|
|
96
|
+
options: {
|
|
97
|
+
responsive: false,
|
|
98
|
+
maintainAspectRatio: false,
|
|
99
|
+
aspectRatio: 2,
|
|
100
|
+
scales: {
|
|
101
|
+
x: {
|
|
102
|
+
grid: {
|
|
103
|
+
display: false
|
|
104
|
+
},
|
|
105
|
+
/*ticks: {
|
|
106
|
+
font: { size: 12 }
|
|
107
|
+
}*/
|
|
108
|
+
},
|
|
109
|
+
y: {
|
|
110
|
+
beginAtZero: true,
|
|
111
|
+
ticks: {
|
|
112
|
+
precision: 0
|
|
113
|
+
//stepSize: 10, font: { size: 12 }
|
|
114
|
+
},
|
|
115
|
+
grid: {
|
|
116
|
+
color: "#eee"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
plugins: {
|
|
121
|
+
title: {
|
|
122
|
+
display: null !== title,
|
|
123
|
+
text: title || '',
|
|
124
|
+
font: { size: 14 },
|
|
125
|
+
color: "#333",
|
|
126
|
+
padding: { top: 10, bottom: 20 }
|
|
127
|
+
},
|
|
128
|
+
legend: {
|
|
129
|
+
display: chartData.datasets.length > 1,
|
|
130
|
+
position: "bottom",
|
|
131
|
+
},
|
|
132
|
+
tooltip: {
|
|
133
|
+
callbacks: {
|
|
134
|
+
label: context => context.dataset.label + ' : ' + context.parsed.y
|
|
135
|
+
//label: (context) => `${context.formattedValue} pointages`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
animation: {
|
|
140
|
+
duration: 1200,
|
|
141
|
+
easing: "easeOutQuart"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, options));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static createLineChart(chartDiv, chartData, title=null, options={}) {
|
|
148
|
+
Chartjs.init();
|
|
149
|
+
|
|
150
|
+
chartDiv.empty();
|
|
151
|
+
new Chart(chartDiv.get(0).getContext("2d"), $.extend(true, {}, {
|
|
152
|
+
type: "line",
|
|
153
|
+
data: {
|
|
154
|
+
labels: chartData.labels,
|
|
155
|
+
datasets: chartData.datasets
|
|
156
|
+
},
|
|
157
|
+
options: {
|
|
158
|
+
responsive: false,
|
|
159
|
+
maintainAspectRatio: false,
|
|
160
|
+
aspectRatio: 2,
|
|
161
|
+
scales: {
|
|
162
|
+
y: {
|
|
163
|
+
beginAtZero: true,
|
|
164
|
+
ticks: {
|
|
165
|
+
precision: 0
|
|
166
|
+
},
|
|
167
|
+
grid: {
|
|
168
|
+
color: "#eee"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
x: {
|
|
172
|
+
grid: {
|
|
173
|
+
display: false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
plugins: {
|
|
178
|
+
title: {
|
|
179
|
+
display: null !== title,
|
|
180
|
+
text: title || '',
|
|
181
|
+
font: { size: 14 },
|
|
182
|
+
color: "#333",
|
|
183
|
+
padding: { top: 10, bottom: 20 }
|
|
184
|
+
},
|
|
185
|
+
legend: {
|
|
186
|
+
display: chartData.datasets.length > 1,
|
|
187
|
+
position: "bottom",
|
|
188
|
+
},
|
|
189
|
+
tooltip: {
|
|
190
|
+
callbacks: {
|
|
191
|
+
label: context => context.dataset.label + ' : ' + context.parsed.y
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, options));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static createDoughnutChart(chartDiv, chartData, title=null, options={}) {
|
|
200
|
+
Chartjs.init();
|
|
201
|
+
|
|
202
|
+
chartDiv.empty();
|
|
203
|
+
new Chart(chartDiv.get(0).getContext("2d"), $.extend(true, {}, {
|
|
204
|
+
type: "doughnut",
|
|
205
|
+
data: {
|
|
206
|
+
labels: chartData.labels,
|
|
207
|
+
datasets: [{
|
|
208
|
+
data: chartData.values,
|
|
209
|
+
backgroundColor: chartData.colors,
|
|
210
|
+
borderWidth: 0,
|
|
211
|
+
hoverOffset: 10
|
|
212
|
+
}]
|
|
213
|
+
},
|
|
214
|
+
options: {
|
|
215
|
+
cutout: "65%",
|
|
216
|
+
responsive: false,
|
|
217
|
+
maintainAspectRatio: false,
|
|
218
|
+
aspectRatio: 2,
|
|
219
|
+
plugins: {
|
|
220
|
+
title: {
|
|
221
|
+
display: null !== title,
|
|
222
|
+
text: title || ''
|
|
223
|
+
},
|
|
224
|
+
legend: {
|
|
225
|
+
position: "right",
|
|
226
|
+
labels: {
|
|
227
|
+
boxWidth: 12,
|
|
228
|
+
font: { size: 12 },
|
|
229
|
+
usePointStyle: true
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
tooltip: {
|
|
233
|
+
callbacks: {
|
|
234
|
+
label: function(context) {
|
|
235
|
+
const total = context.dataset.data.reduce((a,b)=>a+b,0);
|
|
236
|
+
const value = context.raw;
|
|
237
|
+
const percent = ((value / total) * 100).toFixed(1);
|
|
238
|
+
return `${context.label}: ${value} (${percent}%)`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
animation: {
|
|
244
|
+
animateRotate: true,
|
|
245
|
+
animateScale: true
|
|
246
|
+
},
|
|
247
|
+
centerText: {
|
|
248
|
+
display: false,
|
|
249
|
+
fontSize: 18,
|
|
250
|
+
color: "#000"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}, options));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static groupByPeriod(data, period, metrics) {
|
|
257
|
+
const grouped = {};
|
|
258
|
+
|
|
259
|
+
//data = Object.entries(dataObj).map(([date, values]) => ({ date, ...values }));
|
|
260
|
+
|
|
261
|
+
Object.entries(data).forEach(([date, values]) => {
|
|
262
|
+
//data.forEach(entry => {
|
|
263
|
+
const d = new Date(date);
|
|
264
|
+
let key;
|
|
265
|
+
|
|
266
|
+
if (period === 'week') {
|
|
267
|
+
const week = Math.ceil((d.getDate() - d.getDay() + 1) / 7);
|
|
268
|
+
key = `${d.getFullYear()}-S${week}`;
|
|
269
|
+
}
|
|
270
|
+
else if (period === 'month') {
|
|
271
|
+
key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
key = date;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!grouped[key]) {
|
|
278
|
+
grouped[key] = {};
|
|
279
|
+
metrics.forEach(m => grouped[key][m] = []);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
metrics.forEach(m => {
|
|
283
|
+
if (values[m] !== undefined) grouped[key][m].push(values[m]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return Object.entries(grouped).map(([label, vals]) => {
|
|
288
|
+
const aggregated = {};
|
|
289
|
+
metrics.forEach(m => {
|
|
290
|
+
aggregated[m] = vals[m].reduce((a, b) => a + b, 0) / vals[m].length;
|
|
291
|
+
});
|
|
292
|
+
return { label, ...aggregated };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static getPeriodLabels(data, period, locale = 'fr-FR', timeZone = 'Europe/Paris') {
|
|
297
|
+
return DatePeriod.getPeriodLabels(Object.keys(data), period, locale, timeZone);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static getAutoGranularity(data) {
|
|
301
|
+
const dates = Object.keys(data);
|
|
302
|
+
const days = (new Date(dates[dates.length - 1]) - new Date(dates[0])) / (1000 * 60 * 60 * 24);
|
|
303
|
+
if (days > 90) return 'month';
|
|
304
|
+
if (days > 30) return 'week';
|
|
305
|
+
return 'day_of_month';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { Chartjs };
|
package/date_time.js
CHANGED
|
@@ -122,7 +122,7 @@ class DateTime {
|
|
|
122
122
|
static getTimeDisplayWithNbDays(jsDate, jsPreviousDate, locale="fr-FR", timeZone="Europe/Paris") {
|
|
123
123
|
let str = this.getTimeDisplay(jsDate, locale, timeZone);
|
|
124
124
|
if (jsPreviousDate !== 0 && jsPreviousDate != null) {
|
|
125
|
-
let nbDaysDiff =
|
|
125
|
+
let nbDaysDiff = DatePeriod.getNbDayBetweenTwo(jsPreviousDate, jsDate, false);
|
|
126
126
|
if (nbDaysDiff > 0) {
|
|
127
127
|
str += ' (J+'+nbDaysDiff+')';
|
|
128
128
|
}
|
|
@@ -277,6 +277,27 @@ class DateTime {
|
|
|
277
277
|
return jsDateTime > today;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
static addDays(date, days) {
|
|
281
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
282
|
+
return date;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static addMonths(date, months) {
|
|
286
|
+
let d = date.getDate();
|
|
287
|
+
date.setMonth(date.getMonth() + +months);
|
|
288
|
+
if (date.getDate() !== d) {
|
|
289
|
+
date.setDate(0);
|
|
290
|
+
}
|
|
291
|
+
return date;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @deprecated use DatePeriod.getNbDayBetweenTwo instead */
|
|
295
|
+
static getNbDayBetweenTwo(jsDate1, jsDate2, asPeriod=false, timeZone="Europe/Paris") {
|
|
296
|
+
return DatePeriod.getNbDayBetweenTwo(jsDate1, jsDate2, asPeriod, timeZone);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
class DatePeriod {
|
|
280
301
|
static getNbDayBetweenTwo(jsDate1, jsDate2, asPeriod=false, timeZone="Europe/Paris") {
|
|
281
302
|
//jsDate1.set
|
|
282
303
|
if (jsDate1 == null || jsDate2 == null) {
|
|
@@ -299,18 +320,34 @@ class DateTime {
|
|
|
299
320
|
return parseInt(Math.round((timestamp2-timestamp1)/86400));
|
|
300
321
|
}
|
|
301
322
|
|
|
302
|
-
static
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
static getPeriodLabels(data, period, locale = 'fr-FR', timeZone = 'Europe/Paris') {
|
|
324
|
+
if (!data || data.length === 0) {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
306
327
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
328
|
+
if (period === 'month') {
|
|
329
|
+
data.map(yearAndMonth => {
|
|
330
|
+
const [year, month] = yearAndMonth.split('-').map(Number);
|
|
331
|
+
return DateTime.getMonthNameByMonth(month, locale).capitalize()+' '+year;
|
|
332
|
+
});
|
|
312
333
|
}
|
|
313
|
-
|
|
334
|
+
if (period === 'week') {
|
|
335
|
+
return data.map(yearAndWeek => {
|
|
336
|
+
const [year, weekStr] = yearAndWeek.split('-S');
|
|
337
|
+
const week = parseInt(weekStr, 10);
|
|
338
|
+
return 'S'+week+' '+year;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (period === 'day_of_week') {
|
|
342
|
+
return data.map(weekDay => DateTime.getDayNameByDayOfWeek(weekDay, locale).capitalize());
|
|
343
|
+
}
|
|
344
|
+
if (period === 'day_of_month') {
|
|
345
|
+
return data.map(sqlDate => SqlDate.getDateDigitalDisplay(sqlDate, locale, timeZone));
|
|
346
|
+
}
|
|
347
|
+
if (period === 'hour') {
|
|
348
|
+
return data.map(hour => String(hour).padStart(2, '0')+'h');
|
|
349
|
+
}
|
|
350
|
+
return data;
|
|
314
351
|
}
|
|
315
352
|
}
|
|
316
353
|
|
|
@@ -386,7 +423,7 @@ class TimestampUnix {
|
|
|
386
423
|
}
|
|
387
424
|
|
|
388
425
|
static getNbDayBetweenTwo(timestamp1, timestamp2, asPeriod=false, timeZone="Europe/Paris") {
|
|
389
|
-
return
|
|
426
|
+
return DatePeriod.getNbDayBetweenTwo(this.parse(timestamp1), this.parse(timestamp2), asPeriod, timeZone);
|
|
390
427
|
}
|
|
391
428
|
|
|
392
429
|
static isDateInThePast(timestamp) {
|
|
@@ -452,7 +489,7 @@ class SqlDate {
|
|
|
452
489
|
return DateTime.isDateInTheFuture(SqlDateTime.parse(sqlDate + " 00:00:00"));
|
|
453
490
|
}
|
|
454
491
|
static getNbDayBetweenTwo(sqlDate1, sqlDate2, asPeriod=false) {
|
|
455
|
-
return
|
|
492
|
+
return DatePeriod.getNbDayBetweenTwo(SqlDateTime.parse(sqlDate1 + " 00:00:00"), SqlDateTime.parse(sqlDate2 + " 00:00:00"), asPeriod);
|
|
456
493
|
}
|
|
457
494
|
}
|
|
458
495
|
|
|
@@ -585,9 +622,9 @@ class SqlDateTime {
|
|
|
585
622
|
}
|
|
586
623
|
|
|
587
624
|
static getNbDayBetweenTwo(sqlDateTime1, sqlDateTime2, asPeriod=false) {
|
|
588
|
-
return
|
|
625
|
+
return DatePeriod.getNbDayBetweenTwo(this.parse(sqlDateTime1), this.parse(sqlDateTime2), asPeriod);
|
|
589
626
|
}
|
|
590
627
|
|
|
591
628
|
}
|
|
592
629
|
|
|
593
|
-
module.exports = { DateTime, TimestampUnix, SqlDate, SqlTime, SqlDateTime };
|
|
630
|
+
module.exports = { DateTime, DatePeriod, TimestampUnix, SqlDate, SqlTime, SqlDateTime };
|
package/duration.js
CHANGED
|
@@ -69,9 +69,8 @@ class Duration {
|
|
|
69
69
|
|
|
70
70
|
// Minutes
|
|
71
71
|
let strMinute = '';
|
|
72
|
-
let nbMinutes = 0;
|
|
73
72
|
if (withMinutes) {
|
|
74
|
-
nbMinutes = this.getNbMinutesRemainingOfDurationInSeconds(durationInSeconds);
|
|
73
|
+
let nbMinutes = this.getNbMinutesRemainingOfDurationInSeconds(durationInSeconds);
|
|
75
74
|
strMinute += ' ';
|
|
76
75
|
//strMinute += sprintf('%02d', nbMinutes);
|
|
77
76
|
strMinute += nbMinutes.toString().padStart(2, '0');
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const { HTTPRequest, Cookie, UrlAndQueryString } = require('./network');
|
|
|
12
12
|
const { IBAN, BankCard } = require('./bank');
|
|
13
13
|
const { AudioMedia, VideoMedia, UserMedia } = require('./media');
|
|
14
14
|
const { PersonName, Email, TelephoneNumber } = require('./contact_details');
|
|
15
|
-
const { DateTime, TimestampUnix, SqlDate, SqlTime, SqlDateTime } = require('./date_time');
|
|
15
|
+
const { DateTime, DatePeriod, TimestampUnix, SqlDate, SqlTime, SqlDateTime } = require('./date_time');
|
|
16
16
|
const { Duration } = require('./duration');
|
|
17
17
|
const { File, CSV, Img } = require('./file');
|
|
18
18
|
const { FormHelper, EditValue } = require('./form_helper');
|
|
@@ -41,6 +41,7 @@ const { WebRTC } = require('./web_rtc');
|
|
|
41
41
|
const { EventBus } = require('./event_bus');
|
|
42
42
|
|
|
43
43
|
// exports surcouche lib externe
|
|
44
|
+
const { Chartjs } = require('./chartjs');
|
|
44
45
|
const { GoogleCharts } = require('./google_charts');
|
|
45
46
|
const { GoogleRecaptcha } = require('./google_recaptcha');
|
|
46
47
|
const { GoogleMap } = require('./google_maps');
|
|
@@ -49,8 +50,8 @@ const { WebSocket } = require('./web_socket');
|
|
|
49
50
|
|
|
50
51
|
module.exports = {
|
|
51
52
|
Array, Object, Number, String,
|
|
52
|
-
HTTPClient, HTTPRequest, Cookie, UrlAndQueryString, IBAN, BankCard, AudioMedia, VideoMedia, UserMedia, PersonName, Email, TelephoneNumber, DateTime, TimestampUnix, SqlDate, SqlTime, SqlDateTime, Duration, File, CSV, Img, FormHelper, Country, PostalAddress, GeographicCoordinates, HexColor, RgbColor, SocialNetwork, NumberFormatter
|
|
53
|
+
HTTPClient, HTTPRequest, Cookie, UrlAndQueryString, IBAN, BankCard, AudioMedia, VideoMedia, UserMedia, PersonName, Email, TelephoneNumber, DateTime, DatePeriod, TimestampUnix, SqlDate, SqlTime, SqlDateTime, Duration, File, CSV, Img, FormHelper, Country, PostalAddress, GeographicCoordinates, HexColor, RgbColor, SocialNetwork, NumberFormatter,
|
|
53
54
|
Browser, DataTable, Pagination, Navigation, DetailsSubArray, SelectAll, MultipleActionInTable, MultipleActionInDivList, EditValue, FormDate, InputPeriod, ShoppingCart, FlashMessage, CountDown, ImportFromCsv, JwtToken, JwtSession, ApiTokenSession, ListBox, WebRTC, WebSocket, EventBus,
|
|
54
55
|
sleep, refresh, chr, ord, trim, empty,
|
|
55
|
-
GoogleCharts, GoogleRecaptcha, GoogleMap, OpenStreetMap
|
|
56
|
+
Chartjs, GoogleCharts, GoogleRecaptcha, GoogleMap, OpenStreetMap
|
|
56
57
|
};
|