@datarailsshared/dr_renderer 1.2.452 → 1.2.454

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarailsshared/dr_renderer",
3
- "version": "1.2.452",
3
+ "version": "1.2.454",
4
4
  "description": "DataRails charts and tables renderer",
5
5
  "keywords": [
6
6
  "datarails",
@@ -0,0 +1,34 @@
1
+ export class DrChart {
2
+ // A highchartsRenderer injection.
3
+ // Could be removed later when each chart type mirgate to class syntax
4
+ static highchartsRenderer;
5
+
6
+ constructor() {
7
+ if (!DrChart.highchartsRenderer) {
8
+ console.error('The highchartsRenderer instance is not provided!');
9
+ }
10
+ }
11
+
12
+ render(chartOptions, opts) {
13
+ return DrChart.highchartsRenderer.ptCreateElementAndDraw(
14
+ chartOptions,
15
+ opts
16
+ );
17
+ }
18
+
19
+ formatValue(data_type, number_format, value, widget_values_format) {
20
+ return DrChart.highchartsRenderer.formatValue(data_type, number_format, value, widget_values_format)
21
+ }
22
+
23
+ getDefaultValueForChart(type, existing_options) {
24
+ return DrChart.highchartsRenderer.getDefaultValueForChart(type, existing_options);
25
+ }
26
+
27
+ ptCreateBasicLineSeries(pivotData, colors, onlyNumbers, isUniqueVals, additionOptions, opts, chartOptions) {
28
+ return DrChart.highchartsRenderer.ptCreateBasicLineSeries(pivotData, colors, onlyNumbers, isUniqueVals, additionOptions, opts, chartOptions);
29
+ }
30
+
31
+ getSingleValueAgg(opts, aggfunc, base) {
32
+ return DrChart.highchartsRenderer.getSingleValueAgg(opts, aggfunc, base);
33
+ }
34
+ }
@@ -0,0 +1,550 @@
1
+ const { DrChart } = require("./dr_chart");
2
+ const { DrChartTooltip } = require("../dr_chart_tooltip");
3
+ const helpers = require("../dr-renderer-helpers");
4
+
5
+ export const GAUGE_OPTIONS_DEFAULT = {
6
+ gauge: {
7
+ background: "#fff",
8
+ startAngle: -90,
9
+ endAngle: 90,
10
+ thickness: 16,
11
+ tickLength: 40,
12
+ tickWidth: 2,
13
+ valueOffset: [20, 0, 8, 0],
14
+ offset: [8, 8, 8, 8],
15
+ goalIcon:
16
+ "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDE2IDE1IiBmaWxsPSJub25lIj4KICAgIDxwYXRoIGQ9Ik04IDBMOS43OTYxMSA1LjUyNzg2SDE1LjYwODVMMTAuOTA2MiA4Ljk0NDI3TDEyLjcwMjMgMTQuNDcyMUw4IDExLjA1NTdMMy4yOTc3MiAxNC40NzIxTDUuMDkzODMgOC45NDQyN0wwLjM5MTU0OCA1LjUyNzg2SDYuMjAzODlMOCAwWiIgZmlsbD0iIzQ2NDZDRSIvPgo8L3N2Zz4=",
17
+ goalIconSize: [16, 16],
18
+ pivot: {
19
+ radius: 5,
20
+ color: "#808080",
21
+ },
22
+ colors: {
23
+ meta: "#6D6E6F",
24
+ goal: "#4646CE",
25
+ },
26
+ },
27
+ goal: {
28
+ title: "Goal",
29
+ value: 180,
30
+ },
31
+ isAbsoluteValue: false,
32
+ segments: [
33
+ {
34
+ from: 0,
35
+ to: 60,
36
+ title: "Low",
37
+ color: "#BF1D30",
38
+ },
39
+ {
40
+ from: 61,
41
+ to: 80,
42
+ title: "Medium",
43
+ color: "#FFA310",
44
+ },
45
+ {
46
+ from: 81,
47
+ to: 100,
48
+ title: "High",
49
+ color: "#037C5A",
50
+ },
51
+ ],
52
+ };
53
+
54
+ export class DrGaugeChart extends DrChart {
55
+ #aggregation;
56
+ #format;
57
+ #originalOptions;
58
+ #options;
59
+ #ticks;
60
+ #max;
61
+ #goal;
62
+ #value;
63
+ #plotBands;
64
+
65
+ constructor(pivotData, opts) {
66
+ super();
67
+ this.#originalOptions = opts;
68
+ this.#options = this.mergeOptions(opts.chartOptions);
69
+ this.#aggregation = pivotData.getAggregator([], []);
70
+ this.#format = this.#aggregation.widget_values_format;
71
+ this.#goal = this.#options.goal;
72
+ this.#plotBands = this.createPlotBands(this.#options);
73
+ this.#ticks = this.createTicks(this.#plotBands, this.#options);
74
+ this.#value = this.getValue(pivotData, opts);
75
+ this.#max = this.#ticks[this.#ticks.length - 1];
76
+ }
77
+
78
+ render() {
79
+ return super.render(this.configChart(), this.#originalOptions);
80
+ }
81
+
82
+ isLeftQuarter(value, max = this.#max) {
83
+ return value < max / 2;
84
+ }
85
+
86
+ createPlotBands(options) {
87
+ const {
88
+ segments,
89
+ isAbsoluteValue,
90
+ goal: { value: goalValue },
91
+ gauge: { thickness },
92
+ } = options;
93
+
94
+ const bands = segments.map((item) => {
95
+ return {
96
+ from: isAbsoluteValue ? Math.max(0, item.from - 1) : (Math.max(0, item.from - 1) * goalValue) / 100,
97
+ to: isAbsoluteValue ? item.to : (item.to * goalValue) / 100,
98
+ color: item.color,
99
+ thickness: thickness,
100
+ title: item.title,
101
+ };
102
+ });
103
+
104
+ // clamp segments
105
+ bands[0].from = 0;
106
+ bands[bands.length - 1].to = Math.max(bands[bands.length - 1].to, goalValue);
107
+
108
+ return bands;
109
+ }
110
+
111
+ createTicks(plotBands, options) {
112
+ return [...new Set([0, ...plotBands.map((b) => b.to), options.goal.value])].sort((a, b) => a - b);
113
+ }
114
+
115
+ mergeOptions(options) {
116
+ return helpers.mergeDeep(
117
+ JSON.parse(JSON.stringify(GAUGE_OPTIONS_DEFAULT)),
118
+ this.getDefaultValueForChart(DrChart.highchartsRenderer.CHART_TYPES.GAUGE_CHART_ENHANCED),
119
+ options
120
+ );
121
+ }
122
+
123
+ getAngleForValue(
124
+ value,
125
+ min = 0,
126
+ max = this.#max,
127
+ startAngle = this.#options.gauge.startAngle,
128
+ endAngle = this.#options.gauge.endAngle
129
+ ) {
130
+ const degrees = ((value - min) / (max - min)) * (endAngle - startAngle);
131
+ const radians = degrees * (Math.PI / 180);
132
+ return radians;
133
+ }
134
+
135
+ formatValue(value, format = this.#format) {
136
+ return helpers.isNumber(value) ? super.formatValue("n", format, value).value : value;
137
+ }
138
+
139
+ toPercent(value) {
140
+ return `${Math.round((value * 100) / (this.#options.isAbsoluteValue ? this.#max : this.#goal.value))}%`;
141
+ }
142
+
143
+ formatValueLabel(value, options) {
144
+ return `<span style="display: flex; flex-direction: column; align-items: center; gap: 6px; font-size: ${
145
+ options.label.font_size
146
+ }px;">
147
+ <span style="font-weight: 600; font-size: 1.5em; line-height: 1; color: ${options.label.font_color}">
148
+ ${this.formatValue(value)}
149
+ ${
150
+ options.label.show_percentage_in_value
151
+ ? `<span style="font-size: 0.5833em; font-weight: 600; color: ${
152
+ options.gauge.colors.meta
153
+ };">(${this.toPercent(value)})</span>`
154
+ : ""
155
+ }
156
+ </span>
157
+ <span style="font-weight: 500; font-size: 0.875em; line-height: 1; color: ${
158
+ options.gauge.colors.meta
159
+ };">Current status</span>
160
+ </span>`;
161
+ }
162
+
163
+ formatTickLabel(value, options) {
164
+ const isGoal = value === options.goal.value;
165
+ const isLeftQuarter = this.isLeftQuarter(value);
166
+ const formattedValue = this.formatValue(value);
167
+
168
+ const goalStyles = isGoal ? `padding-${isLeftQuarter ? "right" : "left"}: 12px;` : "";
169
+ const goalValue = isGoal
170
+ ? `<span style="font-size: 1.125em; color: ${options.gauge.colors.goal};">${formattedValue}</span>`
171
+ : `<span style="color: ${options.label.font_color};">${formattedValue}</span>`;
172
+ const goalTitle =
173
+ isGoal && options.label.show_goal_name && options.goal.title
174
+ ? `<span style="font-size: 0.75em; color: ${options.gauge.colors.goal};">${options.goal.title}</span>`
175
+ : "";
176
+ const percentage = options.label.show_percentage_in_segments
177
+ ? `<span style="font-size: 0.75em; color: ${options.gauge.colors.meta}; font-weight: 400;">(${this.toPercent(
178
+ value
179
+ )})</span>`
180
+ : "";
181
+
182
+ return `<span style="
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 4px;
186
+ font-weight: 600;
187
+ font-size: ${options.label.font_size}px;
188
+ ${goalStyles}
189
+ ">
190
+ ${goalValue}
191
+ ${goalTitle}
192
+ ${percentage}
193
+ </span>`;
194
+ }
195
+
196
+ getValue(pivotData, opts) {
197
+ const lineSeries = this.ptCreateBasicLineSeries(pivotData, null, true, null, null, opts, {});
198
+
199
+ let total = lineSeries
200
+ .map((s) => s.data.map((v) => v))
201
+ .filter((v) => !isNaN(v))
202
+ .flat();
203
+
204
+ let aggfunc = (a, b) => a + b;
205
+ let base = 0;
206
+ let singleValueAgg = this.getSingleValueAgg(
207
+ {
208
+ value: {
209
+ value: "Sum",
210
+ },
211
+ },
212
+ aggfunc,
213
+ base
214
+ );
215
+
216
+ aggfunc = singleValueAgg.aggfunc;
217
+ base = singleValueAgg.base;
218
+
219
+ const aggregator = pivotData.getAggregator([], []);
220
+ const value = total.length > 0 ? total.reduce(aggfunc, base) : aggregator.value();
221
+
222
+ return value;
223
+ }
224
+
225
+ getBorderPosition(chart, options, position) {
226
+ const { center, size } = chart.pane[0].options;
227
+ const {
228
+ gauge: { tickLength, tickWidth },
229
+ } = options;
230
+
231
+ return {
232
+ x: position === "start" ? center[0] - size / 2 + tickLength / 2 : center[0] + size / 2 - tickLength / 2,
233
+ y: center[1] + tickWidth / 2,
234
+ };
235
+ }
236
+
237
+ createBorder(chart, options, position) {
238
+ return chart.renderer
239
+ .arc({
240
+ ...this.getBorderPosition(chart, options, position),
241
+ r: options.gauge.thickness / 2,
242
+ innerR: 0,
243
+ start: 0,
244
+ end: Math.PI,
245
+ })
246
+ .attr({
247
+ fill:
248
+ position === "start"
249
+ ? chart.yAxis[0].options.plotBands[0].color
250
+ : chart.yAxis[0].options.plotBands.slice(-1)[0].color,
251
+ })
252
+ .add()
253
+ .toFront();
254
+ }
255
+
256
+ getGoalIconPosition(chart, options) {
257
+ const { center, size } = chart.pane[0].options;
258
+ const radius = size / 2;
259
+ const {
260
+ gauge: { goalIconSize },
261
+ goal: { value },
262
+ } = options;
263
+
264
+ return {
265
+ x: center[0] - radius * Math.sin(Math.PI / 2 - this.getAngleForValue(value)) - goalIconSize[0] / 2,
266
+ y: center[1] - radius * Math.cos(Math.PI / 2 - this.getAngleForValue(value)) - goalIconSize[1] / 2,
267
+ };
268
+ }
269
+
270
+ createGoalIcon(chart, options) {
271
+ const point = this.getGoalIconPosition(chart, options);
272
+ return chart.renderer
273
+ .image(options.gauge.goalIcon, point.x, point.y, options.gauge.goalIconSize[0], options.gauge.goalIconSize[1])
274
+ .add()
275
+ .toFront();
276
+ }
277
+
278
+ getValueLabelPosition(chart, options) {
279
+ const { center } = chart.pane[0].options;
280
+ return {
281
+ x: center[0],
282
+ y: center[1] + options.gauge.valueOffset[0],
283
+ };
284
+ }
285
+
286
+ createValueLabel(chart, options) {
287
+ const label = chart.renderer.text(this.formatValueLabel(this.#value, options), 0, 0, true).add().toFront();
288
+
289
+ helpers.removeSVGTextCorrection(label, "yCorr");
290
+
291
+ label.attr(this.getValueLabelPosition(chart, options)).css({
292
+ transform: "translateX(-50%)",
293
+ });
294
+
295
+ return label;
296
+ }
297
+
298
+ getPaneDimensions(chart, options) {
299
+ const { renderer } = chart;
300
+ const valueLabel = this.createValueLabel(chart, this.#options);
301
+ const { offset } = options.gauge;
302
+ const { height: labelH } = valueLabel.getBBox();
303
+
304
+ const offsetBottom = labelH + options.gauge.valueOffset[0] + options.gauge.valueOffset[2] + offset[2];
305
+ valueLabel.destroy();
306
+
307
+ const radiuses = [chart.chartWidth / 2 - Math.max(offset[1], offset[3]), chart.chartHeight - offsetBottom - offset[0]];
308
+ if (options.label.show) {
309
+ this.#ticks.forEach((tick) => {
310
+ const label = renderer.label(this.formatTickLabel(tick, options), 0, 0, null, null, null, true).add();
311
+ const angle = this.getAngleForValue(tick);
312
+ // depends on label width
313
+ radiuses.push(
314
+ (chart.chartWidth / 2 - label.bBox.width - Math.max(offset[1], offset[3])) /
315
+ Math.sin(Math.abs(Math.PI / 2 - angle))
316
+ );
317
+ // depends on label height
318
+ radiuses.push(
319
+ (chart.chartHeight - offsetBottom - label.bBox.height / 2 - offset[0]) /
320
+ Math.cos(Math.abs(Math.PI / 2 - angle))
321
+ );
322
+ label.destroy();
323
+ });
324
+ } else {
325
+ // reserve space for the goal icon
326
+ const angle = this.getAngleForValue(options.goal.value);
327
+ const [iconW, iconH] = options.gauge.goalIconSize;
328
+ radiuses.push((chart.chartWidth / 2 - iconW / 2 - offset[1]) / Math.sin(Math.abs(Math.PI / 2 - angle)));
329
+ radiuses.push((chart.chartHeight - offsetBottom - iconH / 2 - offset[0]) / Math.cos(Math.abs(Math.PI / 2 - angle)));
330
+ }
331
+
332
+ return {
333
+ radius: Math.min(...radiuses),
334
+ center: [chart.chartWidth / 2, chart.chartHeight - offsetBottom],
335
+ };
336
+ }
337
+
338
+ setTicksStyles(chart, options) {
339
+ Object.keys(chart.yAxis[0].ticks).forEach((i) => {
340
+ const tick = chart.yAxis[0].ticks[i];
341
+ const isLeftQuarter = this.isLeftQuarter(tick.pos);
342
+
343
+ if (tick.pos === options.goal.value) {
344
+ tick.mark.attr({
345
+ stroke: options.gauge.colors.goal,
346
+ "stroke-dasharray": 2,
347
+ "pointer-events": "none",
348
+ });
349
+ }
350
+
351
+ if (!tick.label) return;
352
+
353
+ // align left querter's labels
354
+ tick.label.css({
355
+ transform: `translate(${isLeftQuarter ? "-100%" : 0}, 0)`,
356
+ });
357
+ });
358
+ }
359
+
360
+ addTooltips(chart, options) {
361
+ if (!options.tooltips.show) {
362
+ return false;
363
+ }
364
+
365
+ const drTooltip = new DrChartTooltip(chart, {
366
+ fontSize: options.tooltips.font_size,
367
+ fontFamily: options.tooltips.font_style,
368
+ color: options.tooltips.font_color,
369
+ });
370
+
371
+ // segment title tooltip
372
+ if (options.tooltips.show_segment_name) {
373
+ chart.yAxis[0].plotLinesAndBands.forEach((band, i) => {
374
+ drTooltip.add(band.options.title, band.svgElem.element, {
375
+ direction: "top",
376
+ followPointer: true,
377
+ });
378
+ });
379
+ }
380
+
381
+ // value label tooltip
382
+ if (options.tooltips.show_percentage_in_value && !options.label.show_percentage_in_value) {
383
+ drTooltip.add(this.toPercent(this.#value), chart.label.element, {
384
+ direction: "top",
385
+ });
386
+ }
387
+
388
+ // segment name tooltips
389
+ Object.keys(chart.yAxis[0].ticks).forEach((i) => {
390
+ const tick = chart.yAxis[0].ticks[i];
391
+ const isLeftQuarter = this.isLeftQuarter(tick.pos);
392
+
393
+ // goal tooltip if lebels are hidden
394
+ if (tick.pos === options.goal.value && !options.label.show) {
395
+ drTooltip.add(
396
+ `${
397
+ options.label.show_goal_name ? options.goal.title || "" : ""
398
+ }<span style="font-weight: 600">${this.formatValue(options.goal.value)}</span>`,
399
+ chart.goalIcon.element,
400
+ {
401
+ direction: isLeftQuarter ? "left" : "right",
402
+ }
403
+ );
404
+ }
405
+
406
+ // segment label percentage tooltips
407
+ if (tick.label && options.tooltips.show_percentage_in_segments && !options.label.show_percentage_in_segments) {
408
+ drTooltip.add(this.toPercent(tick.pos), tick.label.element, {
409
+ direction: isLeftQuarter ? "left" : "right",
410
+ });
411
+ }
412
+ });
413
+ }
414
+
415
+ setPane(chart, options) {
416
+ const { radius, center } = this.getPaneDimensions(chart, options);
417
+ chart.pane[0].options.size = 2 * radius;
418
+ chart.pane[0].options.center = center;
419
+ chart.yAxis[0].options.plotBands.forEach((band) => {
420
+ band.outerRadius = radius - (options.gauge.tickLength - options.gauge.thickness) / 2;
421
+ });
422
+ chart.series[0].options.dial.radius = Math.round(100 * (radius - options.gauge.tickLength - 10) / radius) + '%';
423
+ }
424
+
425
+ setCustomElements(chart, options) {
426
+ chart.label = this.createValueLabel(chart, options);
427
+ chart.startBorder = this.createBorder(chart, options, "start");
428
+ chart.endBorder = this.createBorder(chart, options, "end");
429
+ chart.goalIcon = this.createGoalIcon(chart, options);
430
+ }
431
+
432
+ updateCustomElements(chart, options) {
433
+ chart.startBorder.attr(this.getBorderPosition(chart, options, "start"));
434
+ chart.endBorder.attr(this.getBorderPosition(chart, options, "end"));
435
+ chart.goalIcon.attr(this.getGoalIconPosition(chart, options));
436
+ chart.label.attr(this.getValueLabelPosition(chart, options));
437
+ }
438
+
439
+ moveCustomElementsToFront(chart) {
440
+ chart.startBorder.toFront();
441
+ chart.endBorder.toFront();
442
+ chart.goalIcon.toFront();
443
+ }
444
+
445
+ clampValueToPane(value, max = this.#max) {
446
+ return helpers.clamp(-0.02 * max, value, 1.02 * max);
447
+ }
448
+
449
+ configChart() {
450
+ return {
451
+ title: {
452
+ text: null,
453
+ },
454
+ subtitle: null,
455
+ exporting: {
456
+ allowHTML: true,
457
+ },
458
+ tooltip: {
459
+ enabled: false,
460
+ },
461
+ chart: {
462
+ type: "gauge",
463
+ backgroundColor: this.#options.gauge.background,
464
+ plotBackgroundColor: null,
465
+ plotBackgroundImage: null,
466
+ plotBorderWidth: 0,
467
+ plotShadow: false,
468
+ events: {
469
+ render: ({ target: chart }) => {
470
+ this.moveCustomElementsToFront(chart);
471
+ this.setTicksStyles(chart, this.#options);
472
+ },
473
+ beforeRedraw: ({ target: chart }) => {
474
+ this.setPane(chart, this.#options);
475
+ this.updateCustomElements(chart, this.#options);
476
+ },
477
+ beforeRender: ({ target: chart }) => {
478
+ this.setPane(chart, this.#options);
479
+ this.setCustomElements(chart, this.#options);
480
+ },
481
+ load: ({ target: chart }) => {
482
+ this.addTooltips(chart, this.#options);
483
+ },
484
+ },
485
+ margin: [0, 0, 0, 0],
486
+ spacing: [0, 0, 0, 0],
487
+ },
488
+
489
+ pane: {
490
+ startAngle: -90,
491
+ endAngle: 90,
492
+ background: null,
493
+ center: [0, 0],
494
+ },
495
+
496
+ // the value axis
497
+ yAxis: {
498
+ min: 0,
499
+ max: this.#max,
500
+ tickPositions: this.#ticks,
501
+ tickPosition: "inside",
502
+ tickColor: this.#options.gauge.background,
503
+ tickLength: this.#options.gauge.tickLength,
504
+ tickWidth: this.#options.gauge.tickWidth,
505
+ minorTickInterval: null,
506
+
507
+ labels: {
508
+ enabled: !!this.#options.label.show,
509
+ distance: 0,
510
+ verticalAlign: "middle",
511
+ allowOverlap: true,
512
+ align: "left",
513
+ style: {
514
+ whiteSpace: "nowrap",
515
+ width: "auto",
516
+ },
517
+ formatter: ({ value }) => {
518
+ return this.formatTickLabel(value, this.#options);
519
+ },
520
+ useHTML: true,
521
+ },
522
+ lineWidth: 0,
523
+ plotBands: this.#plotBands,
524
+ },
525
+
526
+ series: [
527
+ {
528
+ name: null,
529
+ data: [this.clampValueToPane(this.#value)],
530
+ dataLabels: [
531
+ {
532
+ enabled: false,
533
+ },
534
+ ],
535
+ dial: {
536
+ radius: "70%",
537
+ backgroundColor: this.#options.gauge.pivot.color,
538
+ baseWidth: 2 * this.#options.gauge.pivot.radius,
539
+ baseLength: "0%",
540
+ rearLength: "0%",
541
+ },
542
+ pivot: {
543
+ backgroundColor: this.#options.gauge.pivot.color,
544
+ radius: this.#options.gauge.pivot.radius,
545
+ },
546
+ },
547
+ ],
548
+ };
549
+ }
550
+ }
@@ -7,7 +7,52 @@ function capitalize(string) {
7
7
  return string.charAt(0).toUpperCase() + string.slice(1);
8
8
  }
9
9
 
10
+ function clamp(min, v, max) {
11
+ return Math.min(max, Math.max(v, min));
12
+ }
13
+
14
+ function isNumber(n) {
15
+ return !isNaN(parseFloat(n)) && isFinite(n);
16
+ }
17
+
18
+ function mergeDeep(target, ...sources) {
19
+ const isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj);
20
+
21
+ if (!isObject(target)) return target;
22
+
23
+ sources.forEach((source) => {
24
+ if (!isObject(source)) return;
25
+
26
+ Object.keys(source).forEach((key) => {
27
+ const targetValue = target[key];
28
+ const sourceValue = source[key];
29
+
30
+ if (isObject(targetValue) && isObject(sourceValue)) {
31
+ target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
32
+ } else {
33
+ target[key] = sourceValue;
34
+ }
35
+ });
36
+ });
37
+
38
+ return target;
39
+ }
40
+
41
+ function removeSVGTextCorrection(svgEl, corr = 'yCorr') {
42
+ Object.defineProperty(svgEl, corr, {
43
+ set: function() {},
44
+ get: function() {
45
+ return 0;
46
+ }
47
+ });
48
+ return svgEl;
49
+ }
50
+
10
51
  module.exports = {
11
52
  backendSortingKeysAreNotEmpty,
12
53
  capitalize,
54
+ clamp,
55
+ isNumber,
56
+ mergeDeep,
57
+ removeSVGTextCorrection,
13
58
  }