• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 18968041869

31 Oct 2025 09:13AM UTC coverage: 76.918% (-0.01%) from 76.932%
18968041869

Pull #11611

github

web-flow
Merge 1b2ac4428 into f5928b825
Pull Request #11611: #11577 Doc build fixed for node 22. Build strategy

31983 of 49693 branches covered (64.36%)

39738 of 51663 relevant lines covered (76.92%)

37.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.04
/web/client/utils/WidgetsUtils.js
1
/*
2
 * Copyright 2018, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8

9
import {
10
    get,
11
    find,
12
    isNumber,
13
    round,
14
    findIndex,
15
    includes,
16
    isEmpty,
17
    cloneDeep,
18
    omit,
19
    castArray,
20
    pick,
21
    isString,
22
    uniq
23
} from 'lodash';
24
import set from "lodash/fp/set";
25
import { CHARTS_REGEX, TRACES_REGEX, MAPS_REGEX, WIDGETS_MAPS_REGEX, WIDGETS_REGEX } from '../actions/widgets';
26
import { findGroups } from './GraphUtils';
27
import { sameToneRangeColors } from './ColorUtils';
28
import uuidv1 from "uuid/v1";
29
import { arrayUpsert } from "./ImmutableUtils";
30
import { randomInt } from "./RandomUtils";
31
import moment from 'moment';
32
import { dateFormats } from './FeatureGridUtils';
33

34

35
export const FONT = {
1✔
36
    FAMILY: "inherit",
37
    SIZE: 12,
38
    COLOR: "#000000"
39
};
40

41
export const getDependentWidget = (k, widgets) => {
1✔
42
    const [match, id] = WIDGETS_REGEX.exec(k);
17✔
43
    if (match) {
17!
44
        return find(widgets, { id });
17✔
45
    }
46
    return null;
×
47
};
48

49
export const getMapDependencyPath = (k, widgetId, widgetMaps) => {
1✔
50
    let [match, mapId] = MAPS_REGEX.exec(k) || [];
7✔
51
    const { maps } = find(widgetMaps, {id: widgetId}) || {};
7✔
52
    if (match && !isEmpty(maps)) {
7✔
53
        const index = findIndex(maps, { mapId });
4✔
54
        return match.replace(mapId, index);
4✔
55
    }
56
    return k;
3✔
57
};
58

59
export const getWidgetDependency = (k, widgets, maps) => {
1✔
60
    const regRes = WIDGETS_REGEX.exec(k);
5✔
61
    let rest = regRes && regRes[2];
5✔
62
    const widgetId = regRes[1];
5✔
63
    rest = getMapDependencyPath(rest, widgetId, maps);
5✔
64
    const widget = getDependentWidget(k, widgets);
5✔
65
    return rest
5✔
66
        ? get(widget, rest)
67
        : widget;
68
};
69
export const getConnectionList = (widgets = []) => {
1!
70
    return widgets.reduce(
4✔
71
        (acc, curr) => {
72
        // note: check mapSync because dependency map is not actually cleaned
73
            const depMap = (get(curr, "mapSync") && get(curr, "dependenciesMap")) || {};
20✔
74
            const dependencies = Object.keys(depMap).map(k => getDependentWidget(depMap[k], widgets)) || [];
20!
75
            return [
20✔
76
                ...acc,
77
                ...(dependencies
78
                    /**
79
                     * This filter removes temp orphan dependencies, but can not recover connection when the value of the connected element is undefined
80
                     * TODO: remove this filter and clean orphan dependencies
81
                     */
82
                    .filter(d => d !== undefined)
12✔
83
                    .map(d => [curr.id, d.id]))
12✔
84
            ];
85
        }, []);
86
};
87

88
/**
89
 * it checks if a number is higher than threshold and returns a shortened version of it
90
 * @param {number} label to parse
91
 * @param {number} threshold threshold to check if it needs to be rounded
92
 * @param {number} decimals number of decimal to use when rounding
93
 * @return the shortened number plus a suffix or the label is a string is passed
94
*/
95
export const shortenLabel = (label, threshold = 1000, decimals = 1) => {
1✔
96
    if (!isNumber(label)) {
5✔
97
        return label;
1✔
98
    }
99
    let unit;
100
    let number = round(label);
4✔
101
    let add = number.toString().length % 3;
4✔
102
    if (number >= threshold) {
4✔
103
        let trimedDigits = number.toString().length - ( add === 0 ? add + 3 : add );
3✔
104
        let zeroNumber = (trimedDigits) / 3;
3✔
105
        let trimedNumber = number / Math.pow(10, trimedDigits);
3✔
106
        switch (zeroNumber) {
3!
107
        case 1 :
108
            unit = ' K';
1✔
109
            break;
1✔
110
        case 2 :
111
            unit = ' M';
1✔
112
            break;
1✔
113
        case 3 :
114
            unit = ' B';
1✔
115
            break;
1✔
116
        case 4 :
117
            unit = ' T';
×
118
            break;
×
119
        default:
120
            unit = '';
×
121
        }
122
        number = round(trimedNumber, decimals) + unit;
3✔
123
    } else {
124
        number = round(label, Math.abs(4 - number.toString().length));
1✔
125
    }
126
    return number;
4✔
127
};
128

129
export const getWidgetsGroups =  (widgets = []) => {
1!
130
    const groups = findGroups(getConnectionList(widgets));
2✔
131
    const colorsOpts = { base: 190, range: 340, options: { base: 10, range: 360, s: 0.67, v: 0.67 } };
2✔
132
    const colors = sameToneRangeColors(colorsOpts.base, colorsOpts.range, groups.length + 1, colorsOpts.options);
2✔
133
    return groups.map((members, i) => ({
4✔
134
        color: colors[i],
135
        widgets: members
136
    }));
137
};
138

139
/**
140
 * returns default aggregation operations for
141
 * charts that can be used in widgtes and for
142
 * other features
143
 */
144
export const getDefaultAggregationOperations = () => {
1✔
145
    return [
4✔
146
        { value: "Count", label: "widgets.operations.COUNT"},
147
        { value: "Sum", label: "widgets.operations.SUM"},
148
        { value: "Average", label: "widgets.operations.AVG"},
149
        { value: "StdDev", label: "widgets.operations.STDDEV"},
150
        { value: "Min", label: "widgets.operations.MIN"},
151
        { value: "Max", label: "widgets.operations.MAX"}
152
    ];
153
};
154

155
export const CHART_PROPS = ["selectedChartId", "selectedTraceId", "id", "mapSync", "widgetType", "charts", "dependenciesMap", "dataGrid", "title", "description"];
1✔
156

157
const legacyColorsMap = {
1✔
158
    'global.colors.blue': '#0888A1',
159
    'global.colors.red': '#CD4A29',
160
    'global.colors.green': '#29CD2E',
161
    'global.colors.brown': '#CD8029',
162
    'global.colors.purple': '#CD29C7'
163
};
164
const legacyColorsToRamps = {
1✔
165
    'global.colors.blue': 'blues',
166
    'global.colors.red': 'reds',
167
    'global.colors.green': 'greens',
168
    'global.colors.brown': 'ylorbr',
169
    'global.colors.purple': 'purples',
170
    'global.colors.random': 'random'
171
};
172
/**
173
 * Return a default trace style
174
 * @param {string} type trace type one of `line`, `bar` or `pie`
175
 * @param {string} options.color overrides the default color
176
 * @param {string} options.ramp overrides the default color ramp value
177
 * @returns {object} trace style
178
 */
179
export const defaultChartStyle = (type, {
1✔
180
    color = legacyColorsMap['global.colors.blue'],
52✔
181
    ramp = 'blues'
54✔
182
} = {}) => {
183
    if (type === 'pie') {
55✔
184
        return {
11✔
185
            msClassification: {
186
                method: 'uniqueInterval',
187
                intervals: 5,
188
                reverse: false,
189
                ramp
190
            }
191
        };
192
    }
193
    if (type === 'bar') {
44✔
194
        return {
26✔
195
            msMode: 'simple',
196
            line: {
197
                color: 'rgb(0, 0, 0)',
198
                width: 0
199
            },
200
            marker: {
201
                color
202
            }
203
        };
204
    }
205
    return {
18✔
206
        line: {
207
            color,
208
            width: 2
209
        },
210
        marker: {
211
            color,
212
            size: 6
213
        }
214
    };
215
};
216
/**
217
 * Convert autoColorOptions style to trace style
218
 * @param {object} options.autoColorOptions a legacy color value
219
 * @param {string} options.type trace type one of `line`, `bar` or `pie`
220
 * @param {string} options.classificationAttributeType type of the classification attribute
221
 * @returns {object} trace style object
222
 */
223
const applyDefaultStyle = ({ autoColorOptions, type, classificationAttributeType }) => {
1✔
224
    if (!autoColorOptions) {
63✔
225
        return { style: defaultChartStyle(type) };
41✔
226
    }
227
    const method = classificationAttributeType === 'number' ? 'equalInterval' : 'uniqueInterval';
22✔
228
    if (autoColorOptions?.name === 'global.colors.custom') {
22✔
229
        return {
21✔
230
            style: {
231
                ...(type === 'bar' && { msMode: 'classification' }),
33✔
232
                msClassification: {
233
                    method,
234
                    intervals: 5,
235
                    reverse: false,
236
                    ramp: 'viridis',
237
                    defaultColor: autoColorOptions.defaultCustomColor,
238
                    defaultLabel: autoColorOptions.defaultClassLabel,
239
                    classes: (method === 'uniqueInterval'
46✔
240
                        ? autoColorOptions.classification
241
                        : autoColorOptions.rangeClassification) || []
242
                }
243
            }
244
        };
245
    }
246
    if (type === 'pie') {
1!
247
        return {
×
248
            style: {
249
                msClassification: {
250
                    method,
251
                    intervals: 5,
252
                    reverse: false,
253
                    ramp: legacyColorsToRamps[autoColorOptions.name]
254
                }
255
            }
256
        };
257
    }
258
    const color = legacyColorsMap[autoColorOptions.name];
1✔
259
    return { style: defaultChartStyle(type, { color }) };
1✔
260
};
261
/**
262
 * Generate the aggregation data key based on the availability of the aggregation function
263
 * @param {string} options.aggregateFunction aggregation function
264
 * @param {string} options.aggregationAttribute aggregation attribute
265
 * @returns {string} aggregation data key
266
 */
267
export const getAggregationAttributeDataKey = (options = {}) => {
1✔
268
    return !options.aggregateFunction || options.aggregateFunction === 'None'
128✔
269
        ? options.aggregationAttribute || ''
131✔
270
        : `${options.aggregateFunction}(${options.aggregationAttribute})`;
271
};
272
/**
273
 * Generate a new trace with default values
274
 * @param {string} options.type trace type one of `line`, `bar` or `pie`
275
 * @param {string} options.color default color of the trace
276
 * @param {boolean} options.randomColor use a random color if true and `color` is not defined
277
 * @param {string} options.geomProp the geometry property name associated with the layer
278
 * @param {object} options.layer a layer object configuration
279
 * @param {object} options.filter filter object associated with the layer
280
 * @returns {object} new trace object
281
 */
282
export const generateNewTrace = (options) => {
1✔
283
    const type = options?.type || 'bar';
7✔
284
    const color = options?.color
7✔
285
        ? options.color
286
        : options?.randomColor
6!
287
            ? `rgb(${randomInt(255)}, ${randomInt(255)}, ${randomInt(255)})`
288
            : undefined;
289
    return {
7✔
290
        id: uuidv1(),
291
        type,
292
        layer: options?.layer,
293
        ...(options?.geomProp && { geomProp: options.geomProp }),
8✔
294
        ...(options?.filter && { filter: options.filter }),
8✔
295
        options: {},
296
        style: defaultChartStyle(type, { color })
297
    };
298
};
299
/**
300
 * Generate a chart supporting multiple traces structure
301
 * @param {object} chart legacy chart
302
 * @returns {object} chart structure using traces
303
 */
304
export const legacyChartToChartWithTraces = ({
1!
305
    yAxis,
306
    xAxisAngle,
307
    type,
308
    options: chartOptions,
309
    autoColorOptions,
310
    legend,
311
    cartesian,
312
    chartId,
313
    xAxisOpts,
314
    yAxisOpts: yAxisOptsProp,
315
    yAxisLabel,
316
    tickPrefix,
317
    format,
318
    tickSuffix,
319
    formula,
320
    name,
321
    geomProp,
322
    layer,
323
    barChartType
324
} = {}) => {
325
    const { classificationAttributeType, ...options } = chartOptions || {};
63!
326
    const {
327
        textinfo,
328
        includeLegendPercent,
329
        ...yAxisOpts
330
    } = yAxisOptsProp || {};
63✔
331
    return {
63✔
332
        name,
333
        legend,
334
        cartesian,
335
        chartId,
336
        barChartType,
337
        xAxisOpts: [{
338
            ...xAxisOpts,
339
            angle: xAxisAngle,
340
            id: 0
341
        }],
342
        yAxisOpts: [{
343
            ...yAxisOpts,
344
            hide: yAxis === false,
345
            id: 0
346
        }],
347
        traces: [{
348
            id: `trace-${chartId}`,
349
            name: yAxisLabel,
350
            type,
351
            options,
352
            ...applyDefaultStyle({
353
                autoColorOptions,
354
                classificationAttributeType,
355
                type
356
            }),
357
            textinfo,
358
            tickPrefix,
359
            format,
360
            tickSuffix,
361
            formula,
362
            geomProp,
363
            layer,
364
            includeLegendPercent
365
        }]
366
    };
367
};
368

369
/**
370
 * Convert the dependenciesMapping to support maplist
371
 * widget for compatibility
372
 * @param data {object} response from dashboard query
373
 * @returns {object} data with updated map widgets
374
 */
375
export const convertDependenciesMappingForCompatibility = (data) => {
1✔
376
    const mapDependencies = ["layers", "viewport", "zoom", "center"];
5✔
377
    const _data = cloneDeep(data);
5✔
378
    const widgets = _data?.widgets || [];
5!
379
    const tempWidgetMapDependency = [];
5✔
380
    return {
5✔
381
        ..._data,
382
        widgets: widgets.map(w => {
383
            let widget = {...w};
6✔
384
            if (w.widgetType === 'map' && w.map) {
6✔
385
                const mapId = uuidv1(); // Add mapId to existing map data
1✔
386
                widget = omit({...w, selectedMapId: mapId, maps: castArray({...w.map, mapId})}, 'map');
1✔
387
                tempWidgetMapDependency.push({widgetId: widget.id, mapId});
1✔
388
            }
389
            if (w.widgetType === 'chart' && w.layer) {
6✔
390
                const chartId = uuidv1(); // Add chartId to existing chart data
1✔
391
                const chartData = omit(widget, CHART_PROPS) || {};
1!
392
                const editorData = pick(widget, CHART_PROPS) || {};
1!
393
                widget = {
1✔
394
                    ...editorData,
395
                    selectedChartId: chartId,
396
                    charts: castArray({...chartData, layer: w.layer, name: 'Chart-1', chartId })
397
                };
398
            }
399
            if (w.widgetType === 'chart' && widget?.charts?.find(chart => !chart.traces)) {
6✔
400
                widget.charts = widget.charts.map((chart) => {
1✔
401
                    if (chart.traces) {
1!
402
                        return chart;
×
403
                    }
404
                    return legacyChartToChartWithTraces(chart);
1✔
405
                });
406
            }
407
            if (!isEmpty(widget.dependenciesMap)) {
6✔
408
                const widgetPath = Object.values(widget.dependenciesMap)[0];
2✔
409
                const [, dependantWidgetId] = WIDGETS_REGEX.exec(widgetPath) || [];
2!
410
                const {widgetId, mapId} = find(tempWidgetMapDependency, {widgetId: dependantWidgetId}) || {};
2✔
411
                if (widgetId) {
2✔
412
                    return {
1✔
413
                        ...widget,
414
                        // Update dependenciesMap containing `map` as dependency
415
                        dependenciesMap: Object.keys(widget.dependenciesMap)
416
                            .filter(k => widget.dependenciesMap[k] !== undefined)
5✔
417
                            .reduce((dm, k) => {
418
                                if (includes(mapDependencies, k)) {
5✔
419
                                    return {
3✔
420
                                        ...dm,
421
                                        [k]: widget.dependenciesMap[k].replace(".map.", `.maps[${mapId}].`)
422
                                    };
423
                                }
424
                                return {...dm, [k]: widget.dependenciesMap[k]};
2✔
425
                            }, {})
426
                    };
427
                }
428
            }
429
            return widget;
5✔
430
        })
431
    };
432
};
433

434
/**
435
 * Update the dependenciesMap of the widgets containing map as dependencies
436
 * when a map is changed in the widget via map switcher
437
 * widget for compatibility
438
 * @param allWidgets {object[]} response from dashboard query
439
 * @param widgetId {string} widget id of map list
440
 * @param selectedMapId {string} selected map id
441
 * @returns {object[]} updated widgets
442
 */
443
export const updateDependenciesMapOfMapList = (allWidgets = [], widgetId, selectedMapId) => {
1!
444
    let widgets = [...allWidgets];
4✔
445
    const widgetsWithDependenciesMaps = widgets.filter(t => t.dependenciesMap);
8✔
446
    const isUpdateNeeded = widgetsWithDependenciesMaps.some(t => Object.values(t.dependenciesMap).some(td => (WIDGETS_REGEX.exec(td) || [])[1] === widgetId));
4!
447
    if (isUpdateNeeded) {
4!
448
        widgets = widgets.map(widget => {
4✔
449
            const dependenciesMap = widget.dependenciesMap;
8✔
450
            const modifiedWidgetId = !isEmpty(dependenciesMap) && (WIDGETS_REGEX.exec(Object.values(dependenciesMap)[0]) || [])[1];
8!
451
            return {
8✔
452
                ...widget,
453
                ...(!isEmpty(dependenciesMap) && modifiedWidgetId === widgetId && {
16✔
454
                    dependenciesMap: Object.keys(dependenciesMap).reduce((dm, k) => {
455
                        const [,, mapIdToReplace] = WIDGETS_MAPS_REGEX.exec(dependenciesMap[k]) || [];
20✔
456
                        if (mapIdToReplace) {
20✔
457
                            return {
12✔
458
                                ...dm,
459
                                [k]: dependenciesMap[k].replace(mapIdToReplace, selectedMapId) // Update map id of the dependenciesMap
460
                            };
461
                        }
462
                        return {...dm, [k]: dependenciesMap[k]};
8✔
463

464
                    }, {})})
465
            };
466
        });
467
    }
468
    return widgets;
4✔
469
};
470

471
/**
472
 * Generate widget editor props
473
 * @param {object} action
474
 * @returns {object} updated editor change props
475
 */
476
export const editorChangeProps = (action) => {
1✔
477
    let key = action.key;
20✔
478
    let pathProp = key;
20✔
479
    const value = action.value;
20✔
480
    let regex = '';
20✔
481
    let identifier = '';
20✔
482
    if (key.includes('maps')) {
20✔
483
        pathProp = 'maps';
7✔
484
        regex = MAPS_REGEX;
7✔
485
        identifier = 'mapId';
7✔
486
    } else if (key.includes('charts')) {
13✔
487
        pathProp = 'charts';
7✔
488
        regex = CHARTS_REGEX;
7✔
489
        identifier = 'chartId';
7✔
490
    }
491
    return { path: `builder.editor.${pathProp}`, value, key, regex, identifier };
20✔
492
};
493

494
/**
495
 * Chart widget specific operation to perform multi chart management
496
 * @param {object} editorData
497
 * @param {string} key
498
 * @param {any} value
499
 * @returns {*}
500
 */
501
const chartWidgetOperation = ({ editorData, key, value }) => {
1✔
502

503
    const editorProp = pick(editorData, CHART_PROPS) || {};
3!
504

505
    if (key === 'chart-layers') {
3✔
506
        const charts = value?.map((v) => ({
2✔
507
            chartId: uuidv1(),
508
            traces: [generateNewTrace({
509
                layer: v
510
            })]
511
        }));
512
        return {
1✔
513
            ...editorProp,
514
            charts,
515
            selectedChartId: charts?.[0]?.chartId,
516
            selectedTraceId: charts?.[0]?.traces?.[0]?.id
517
        };
518
    }
519
    if (key === 'chart-delete') {
2✔
520
        const charts = value;
1✔
521
        return {
1✔
522
            ...editorProp,
523
            charts,
524
            selectedChartId: charts?.[0]?.chartId,
525
            selectedTraceId: charts?.[0]?.traces?.[0]?.id
526
        };
527
    }
528
    if (key === 'chart-add') {
1!
529
        const newCharts = value?.map(v => ({
3✔
530
            chartId: uuidv1(),
531
            traces: [generateNewTrace({
532
                layer: v
533
            })]
534
        }));
535
        const charts = [ ...(editorProp?.charts || []), ...newCharts ];
1!
536
        return {
1✔
537
            ...editorProp,
538
            charts,
539
            selectedChartId: newCharts?.[0]?.chartId || charts?.[0]?.chartId,
1!
540
            selectedTraceId: newCharts?.[0]?.traces?.[0]?.id || charts?.[0]?.traces?.[0]?.id
1!
541
        };
542
    }
543
    if (key === 'chart-layer-replace') {
×
544
        const layer = value.layer[0];
×
545
        const charts = (editorProp.charts || []).map((chart) => {
×
546
            if (chart.chartId === value.chartId) {
×
547
                return {
×
548
                    ...chart,
549
                    traces: (chart?.traces || []).map((trace) => {
×
550
                        if (trace.id === value.traceId) {
×
551
                            return { ...trace, layer, options: {} };
×
552
                        }
553
                        return trace;
×
554
                    })
555
                };
556
            }
557
            return chart;
×
558
        });
559
        return {
×
560
            ...editorProp,
561
            charts
562
        };
563
    }
564

565
    return editorProp;
×
566
};
567

568
// Add value to trace[id] paths
569
const insertTracesOnEditorChange = ({
1✔
570
    identifier,
571
    id,
572
    charts,
573
    pathToUpdate,
574
    value
575
}) => {
576
    if (pathToUpdate.includes('traces[')) {
4!
577
        const currentChart = charts.find(m => m[identifier] === id);
×
578
        const traces = get(currentChart, 'traces', []);
×
579
        const [, traceId, tracePathToUpdate] = TRACES_REGEX.exec(pathToUpdate) || [];
×
580
        const tracesIds = traces.map((trace) => trace.id);
×
581
        const traceIndex = tracesIds.indexOf(traceId);
×
582
        if (traceIndex > -1) {
×
583
            const newTraces = traces.map((trace) => trace.id === traceId ? set(tracePathToUpdate, value, trace) : trace);
×
584
            return set('traces', newTraces, charts.find(m => m[identifier] === id));
×
585
        }
586
    }
587
    return set(pathToUpdate, value, charts.find(m => m[identifier] === id));
4✔
588
};
589

590
/**
591
 * Perform state with widget editor changes
592
 * @param {object} action
593
 * @param {object} state object
594
 * @returns {object|object[]} updated state
595
 */
596
export const editorChange = (action, state) => {
1✔
597
    const { key, path, identifier, regex, value } = editorChangeProps(action);
13✔
598
    // Update multi widgets (currently charts and maps)
599
    if (['maps', 'charts'].some(k => key.includes(k))) {
22✔
600
        if (key === 'maps' && value === undefined) {
8!
601
            return set(path, value, state);
×
602
        }
603
        const [, id, pathToUpdate] = regex.exec(key) || [];
8✔
604
        let updatedValue = value;
8✔
605
        if (id) {
8✔
606
            const editorArray = get(state, path, []);
4✔
607
            updatedValue = insertTracesOnEditorChange({
4✔
608
                identifier,
609
                id,
610
                charts: editorArray,
611
                pathToUpdate,
612
                value
613
            });
614
        }
615
        return arrayUpsert(path, updatedValue, {[identifier]: id || value?.[identifier]}, state);
8✔
616
    }
617
    const editorData = { ...state?.builder?.editor };
5✔
618
    // Widget specific editor changes
619
    if (key.includes(`chart-`)) {
5✔
620
        // TODO Allow to support all widget types that might support multi widget feature
621
        return set('builder.editor', chartWidgetOperation({key, value, editorData}), state);
3✔
622
    }
623
    return set(path, value, state);
2✔
624
};
625

626
export const getDependantWidget = ({widgets = [], dependenciesMap = {}}) =>
1!
627
    widgets?.find(w => w.id === (WIDGETS_REGEX.exec(Object.values(dependenciesMap)?.[0]) || [])[1]) || {};
3!
628

629
/**
630
 * Get the current selected trace data
631
 * @param {object} chart widget chart data
632
 * @param {string} chart.selectedChartId selected chart identifier
633
 * @param {string} chart.selectedTraceId selected traces identifier
634
 * @param {array} chart.charts widget charts
635
 * @returns {object} selected trace data
636
 */
637
export const extractTraceData = ({ selectedChartId, selectedTraceId, charts } = {}) => {
1✔
638
    const selectedChart = (charts || []).find(chart => chart.chartId === selectedChartId);
39✔
639
    const selectedTrace = (selectedChart?.traces || []).find(trace => trace.id === selectedTraceId);
39✔
640
    return selectedTrace || selectedChart?.traces?.[0];
39✔
641
};
642
/**
643
 * Get editing widget from widget data with multi support
644
 * @param {object} widget editing widget
645
 * @returns {object} selected widget data
646
 */
647
export const getSelectedWidgetData = (widget = {}) => {
1!
648
    if (widget.widgetType === 'chart' || widget.charts) {
9✔
649
        const widgetData = extractTraceData(widget);
2✔
650
        return widgetData;
2✔
651
    }
652
    if (widget.widgetType === 'map' || widget.maps) {
7✔
653
        return widget?.maps?.find(c => c.mapId === widget?.selectedMapId) || {};
1!
654
    }
655
    return widget;
6✔
656
};
657

658
export const DEFAULT_MAP_SETTINGS = {
1✔
659
    projection: 'EPSG:900913',
660
    units: 'm',
661
    center: {
662
        x: 11.22894105149402,
663
        y: 43.380053862794,
664
        crs: 'EPSG:4326'
665
    },
666
    maxExtent: [
667
        -20037508.34,
668
        -20037508.34,
669
        20037508.34,
670
        20037508.34
671
    ],
672
    mapId: null,
673
    size: {
674
        width: 1300,
675
        height: 920
676
    },
677
    version: 2,
678
    limits: {},
679
    mousePointer: 'pointer',
680
    resolutions: [
681
        156543.03392804097,
682
        78271.51696402048,
683
        39135.75848201024,
684
        19567.87924100512,
685
        9783.93962050256,
686
        4891.96981025128,
687
        2445.98490512564,
688
        1222.99245256282,
689
        611.49622628141,
690
        305.748113140705,
691
        152.8740565703525,
692
        76.43702828517625,
693
        38.21851414258813,
694
        19.109257071294063,
695
        9.554628535647032,
696
        4.777314267823516,
697
        2.388657133911758,
698
        1.194328566955879,
699
        0.5971642834779395,
700
        0.29858214173896974,
701
        0.14929107086948487,
702
        0.07464553543474244,
703
        0.03732276771737122,
704
        0.01866138385868561,
705
        0.009330691929342804,
706
        0.004665345964671402,
707
        0.002332672982335701,
708
        0.0011663364911678506,
709
        0.0005831682455839253,
710
        0.00029158412279196264,
711
        0.00014579206139598132
712
    ]
713
};
714
// add labels and utils to classification classes
715
const parseClasses = (classes, {
1✔
716
    legendValue
717
} = {}) => {
718
    return classes.map((entry, idx, arr) => ({
94✔
719
        ...entry,
720
        index: idx,
721
        ...(entry.unique !== undefined
94✔
722
            ? {
723
                label: entry.title || entry.unique,
140✔
724
                insideClass: (value) => value === entry.unique
218✔
725
            }
726
            : {
727
                label: (entry.title || (
28✔
728
                    idx < arr.length - 1
10✔
729
                        ? `>= ${entry.min}<br>< ${entry.max}`
730
                        : `>= ${entry.min}<br><= ${entry.max}`
731
                )),
732
                insideClass: (value) => idx < arr.length - 1
74✔
733
                    ? value >= entry.min && value < entry.max
100✔
734
                    : value >= entry.min && value <= entry.max
48✔
735
            })
736
    })).map((entry) => ({
94✔
737
        ...entry,
738
        label: `${entry.label || ''}`
94!
739
            .replace('${minValue}', entry.min ?? '')
170✔
740
            .replace('${maxValue}', entry.max ?? '')
170✔
741
            .replace('${legendValue}', legendValue || '')
153✔
742
    }));
743
};
744
// return correct sorting keys for classification based on the trace type
745
const getSortingKeys = ({ type, options, sortBy }) => {
1✔
746
    if (type === 'pie') {
35✔
747
        const labelDataKey = options?.groupByAttributes;
22✔
748
        const valueDataKey = getAggregationAttributeDataKey(options);
22✔
749
        const classificationDataKey = options?.classificationAttribute || labelDataKey;
22✔
750
        const sortByKey = sortBy !== 'groupBy' ?  valueDataKey : labelDataKey;
22✔
751
        const isNestedPieChart = !(classificationDataKey === labelDataKey);
22✔
752
        const sortKey = isNestedPieChart ? classificationDataKey : sortByKey;
22✔
753
        // we need to reverse the order when sorting by value data key
754
        // in this way we see the bigger slice as first from the top
755
        // with the other displayed clockwise
756
        const sortFunc = sortByKey === valueDataKey
22✔
757
            ? (a, b) => a.index > b.index ? -1 : 1
81✔
758
            : (a, b) => a.index > b.index ? 1 : -1;
16✔
759
        const legendValue = classificationDataKey;
22✔
760
        return {
22✔
761
            sortKey,
762
            sortByKey,
763
            classificationDataKey,
764
            legendValue,
765
            customSortFunc: !isNestedPieChart && sortFunc
37✔
766
        };
767
    }
768
    if (type === 'bar') {
13!
769
        const xDataKey = options?.groupByAttributes;
13✔
770
        const classificationDataKey = options?.classificationAttribute || xDataKey;
13✔
771
        const yDataKey = getAggregationAttributeDataKey(options);
13✔
772
        const sortByKey = sortBy !== 'aggregation' ?  xDataKey : yDataKey;
13✔
773
        const sortKey = classificationDataKey === xDataKey
13✔
774
            ? sortByKey
775
            : classificationDataKey;
776
        const legendValue = yDataKey;
13✔
777
        return { sortKey, sortByKey, classificationDataKey, legendValue };
13✔
778
    }
779
    return {};
×
780
};
781
/**
782
 * Get editing widget from widget data with multi support
783
 * @param {object} widget editing widget
784
 * @returns {object} selected widget data
785
 */
786
export const generateClassifiedData = ({
1✔
787
    type,
788
    data,
789
    sortBy,
790
    options,
791
    msClassification,
792
    classifyGeoJSON,
793
    excludeOthers,
794
    applyCustomSortFunctionOnClasses
795
}) => {
796
    const {
797
        ramp = 'viridis',
2✔
798
        intervals = 5,
4✔
799
        reverse,
800
        method: methodStyle = 'uniqueInterval',
2✔
801
        classes: classesStyle,
802
        defaultColor = '#ffff00',
15✔
803
        defaultLabel = 'Others'
15✔
804
    } = msClassification || {};
35✔
805

806
    const { customSortFunc, sortKey, sortByKey, classificationDataKey, legendValue } = getSortingKeys({ type, options, sortBy });
35✔
807

808
    const customClasses = classesStyle && parseClasses(classesStyle, {
35✔
809
        legendValue
810
    });
811
    const isStringData = isString(data?.[0]?.[classificationDataKey]);
35✔
812
    const sortFunc = data.type === 'pie'
35!
813
        ? (a, b) => a[sortKey] > b[sortKey] ? -1 : 1
×
814
        : (a, b) => a[sortKey] > b[sortKey] ? 1 : -1;
175✔
815
    const initialSortedData = [...data].sort(sortFunc);
35✔
816
    const method = isStringData
35✔
817
        ? 'uniqueInterval'
818
        : methodStyle;
819
    const computedClasses = (customClasses || parseClasses(
35✔
820
        classifyGeoJSON({
821
            type: 'FeatureCollection',
822
            features: initialSortedData.map((properties) => ({ properties, type: 'Feature', geometry: null }))
79✔
823
        }, {
824
            attribute: classificationDataKey,
825
            method,
826
            ramp,
827
            reverse,
828
            intervals,
829
            sort: false
830
        })
831
    ));
832
    const othersClass = {
35✔
833
        color: defaultColor,
834
        label: (defaultLabel || '')
38✔
835
            .replace('${legendValue}', legendValue || ''),
35!
836
        index: computedClasses.length
837
    };
838
    const classifiedData = initialSortedData.map((properties) => {
35✔
839
        const entry = computedClasses.find(({ insideClass }) => insideClass(properties[classificationDataKey]));
292✔
840
        const selectedEntry = entry ? entry : othersClass;
143✔
841
        return {
143✔
842
            ...selectedEntry,
843
            properties,
844
            label: (selectedEntry.label || '').replace('${groupByValue}', properties[options?.groupByAttributes] || '')
288!
845
        };
846
    });
847
    const classes = excludeOthers
35!
848
        ? computedClasses
849
        : [...computedClasses, othersClass];
850
    return {
35✔
851
        sortByKey,
852
        classes: customSortFunc && applyCustomSortFunctionOnClasses
85!
853
            ? classes.sort(customSortFunc)
854
            : classes,
855
        classifiedData: customSortFunc
35✔
856
            ? classifiedData.sort(customSortFunc)
857
            : classifiedData
858
    };
859
};
860
/**
861
 * Ensure a valid number is returned
862
 * @param {number} num a number
863
 * @returns {number} valid number
864
 */
865
export const parseNumber = (num) => isNumber(num) && !isNaN(num) ? num : 0;
63!
866
/**
867
 * Perform a sum aggregation for pie chart data with no aggregation
868
 * to get the correct number of slices
869
 * @param {object} data chart data
870
 * @param {string} options.groupByAttributes group by attribute
871
 * @param {string} options.aggregationAttribute aggregation attribute
872
 * @param {string} options.aggregateFunction aggregate function
873
 * @param {string} options.classificationAttribute classification function
874
 * @returns {object} aggregated data
875
 */
876
export const parsePieNoAggregationFunctionData = (data, options = {}) => {
1✔
877
    const labelDataKey = options?.groupByAttributes;
22✔
878
    const valueDataKey = getAggregationAttributeDataKey(options);
22✔
879
    const hasAggregateFunction = !(!options.aggregateFunction || options.aggregateFunction === 'None');
22!
880
    const classificationDataKey = options?.classificationAttribute || labelDataKey;
22✔
881
    const isNestedPieChart = !(classificationDataKey === labelDataKey);
22✔
882
    if (data && !hasAggregateFunction && !isNestedPieChart) {
22✔
883
        // we need to sum value with the same label property
884
        // if the aggregation is missing
885
        // this is needed to get correct number of slices
886
        const labelProperties = uniq(data.map((properties) => properties[labelDataKey]));
59✔
887
        return labelProperties.map((labelProperty) => {
14✔
888
            const filteredData = data.filter(properties => properties[labelDataKey] === labelProperty);
241✔
889
            return {
54✔
890
                [labelDataKey]: labelProperty,
891
                [valueDataKey]: filteredData.reduce((sum, properties) => sum + parseNumber(properties[valueDataKey]), 0)
59✔
892
            };
893
        });
894
    }
895
    return data;
8✔
896
};
897
/**
898
 * Verify validity of a chart
899
 * @param {string} options.groupByAttributes group by attribute
900
 * @param {string} options.aggregationAttribute aggregation attribute
901
 * @param {string} options.aggregateFunction aggregate function
902
 * @param {string} options.classificationAttribute classification function
903
 * @param {boolean} props.hasAggregateProcess true if the associated service has aggregation
904
 * @returns {boolean} true if valid
905
 */
906
export const isChartOptionsValid = (options = {}, { hasAggregateProcess }) => {
1✔
907
    return !!(
14✔
908
        options.aggregationAttribute
37✔
909
        && options.groupByAttributes
910
        // if aggregate process is not present, the aggregateFunction is not necessary. if present, is mandatory
911
        && (!hasAggregateProcess || hasAggregateProcess && options.aggregateFunction)
912
        || options.classificationAttribute
913
    );
914
};
915
/**
916
 * Verify if the bar chart stack option can be enabled
917
 * @param {string} chart a widget chart configuration
918
 * @returns {boolean} true if the option can be enabled
919
 */
920
export const enableBarChartStack = (chart = {}) => {
1✔
921
    const barTraces = (chart?.traces || [])?.filter(trace => trace.type === 'bar');
83✔
922
    // if there is only one bar chart
923
    // allow the stack selection only for classification
924
    if (barTraces.length === 1) {
80✔
925
        return barTraces[0]?.style?.msMode === 'classification';
31✔
926
    }
927
    // if there is a single x/y axis
928
    // and multiple bar charts
929
    // allow the stack option
930
    if (barTraces.length > 1
49✔
931
        && (chart.xAxisOpts || [{ id: 0 }]).length === 1
9✔
932
        && (chart.yAxisOpts || [{ id: 0 }]).length === 1) {
6✔
933
        return true;
3✔
934
    }
935
    // in other all other cases allow only group
936
    // the reason is related to the overlay behavior for each new axis added
937
    return false;
46✔
938
};
939

940
/**
941
 * Get names of the layers used in the widget
942
 * @param {object} widget current widget object
943
 * @returns {string[]} array of widget's layers name
944
 */
945
export const getWidgetLayersNames = (widget) => {
1✔
946
    const type = widget?.widgetType;
31✔
947
    if (!isEmpty(widget)) {
31✔
948
        if (type !== 'map') {
28✔
949
            if (type === 'chart') {
22✔
950
                return uniq(get(widget, 'charts', [])
13✔
951
                    .map(c => get(c, 'traces', []).map(t => get(t, 'layer.name', '')))
17✔
952
                    .flat()
953
                    .filter(n => n)
17✔
954
                );
955
            }
956
            return castArray(get(widget, 'layer.name', []));
9✔
957
        }
958
        return uniq(get(widget, 'maps', [])
6✔
959
            .map(m => get(m, 'layers', []).map(t => get(t, 'name', '')))
4✔
960
            .flat()
961
            .filter(n => n)
4✔
962
        );
963
    }
964
    return [];
3✔
965
};
966

967
/**
968
 * Check if chart widget layers are compatible with table widget layer
969
 * @param {object} widget current widget object
970
 * @param {object} tableWidget dependant table widget object
971
 * @returns {boolean} flag determines if compatible
972
 */
973
export const isChartCompatibleWithTableWidget = (widget, tableWidget) => {
1✔
974
    const tableLayerName = tableWidget?.layer?.name;
13✔
975
    return tableLayerName && get(widget, 'charts', [])
13✔
976
        .every(({ traces = [] } = {}) => traces
11!
977
            .every(trace => get(trace, 'layer.name') === tableLayerName));
16✔
978
};
979

980
/**
981
 * Check if a table widget can be a depedency to the widget currently is edit
982
 * @param {object} widget current widget in edit
983
 * @param {object} dependencyTableWidget target widget in check for dependency compatibility
984
 * @returns {boolean} flag determines if compatible
985
 */
986
export const canTableWidgetBeDependency = (widget, dependencyTableWidget) => {
1✔
987
    const isChart = widget && widget.widgetType === 'chart';
17✔
988
    const isMap = widget && widget.widgetType === 'map';
17✔
989
    const editingLayer = getWidgetLayersNames(widget);
17✔
990

991
    if (isMap) {
17✔
992
        return !isEmpty(editingLayer);
4✔
993
    }
994
    const layerPresent = editingLayer.includes(get(dependencyTableWidget, 'layer.name'));
13✔
995
    return isChart ? layerPresent && isChartCompatibleWithTableWidget(widget, dependencyTableWidget) : layerPresent;
13✔
996
};
997

998
function findWidgetById(widgets, widgetId) {
999
    return widgets?.find(widget => widget.id === widgetId);
11✔
1000
}
1001

1002
/**
1003
 * Checks if a widget, referenced by `mapSync` in the `dependenciesMap`, has `widgetType` set to `'map'`.
1004
 * If the widget has a `dependenciesMap`, it will be checked recursively.
1005
 *
1006
 * @param {Array<Object>} widgets - List of widget objects, each containing an `id`, `widgetType`, and optionally `dependenciesMap`.
1007
 * @param {Object} dependenciesMap - An object containing a `mapSync` reference to another widget's `mapSync` (e.g., "widgets[widgetId].mapSync").
1008
 * @returns {boolean} - Returns boolean
1009
 *
1010
 * @example
1011
 * checkMapSyncWithWidgetOfMapType(widgets, { mapSync: 'widgets[40fdb720-b228-11ef-974d-8115935269b7].mapSync' });
1012
 */
1013
export function checkMapSyncWithWidgetOfMapType(widgets, dependenciesMap) {
1014
    const mapSyncDependencies = dependenciesMap?.mapSync;
10✔
1015

1016
    if (!mapSyncDependencies) {
10!
1017
        return false;
×
1018
    }
1019
    if (mapSyncDependencies.includes("map.mapSync")) {
10✔
1020
        return true;
4✔
1021
    }
1022
    // Extract widget ID
1023
    const widgetId = mapSyncDependencies.match?.(/\[([^\]]+)\]/)?.[1];
6✔
1024
    if (!widgetId) {
6!
1025
        return false;
×
1026
    }
1027
    // Find the widget using the extracted widgetId
1028
    const widget = findWidgetById(widgets, widgetId);
6✔
1029
    if (!widget) {
6!
1030
        return false;
×
1031
    }
1032
    // Check if the widget has widgetType 'map'
1033
    if (widget.widgetType === 'map') {
6✔
1034
        return true;
5✔
1035
    }
1036
    // If widget has its own dependenciesMap, recursively check that map
1037
    if (widget.dependenciesMap) {
1!
1038
        return checkMapSyncWithWidgetOfMapType(widgets, widget.dependenciesMap);
×
1039
    }
1040
    // If no match found, return false
1041
    return false;
1✔
1042
}
1043

1044
const createRectShape = (axisId, axisType, startTime, endTime, fill = {}) => {
1!
1045
    const isX = axisType === 'x';
2✔
1046
    return {
2✔
1047
        type: 'rect',
1048
        xref: isX ? axisId : 'paper',
2✔
1049
        yref: isX ? 'paper' : axisId,
2✔
1050
        x0: isX ? startTime : 0,
2✔
1051
        x1: isX ? endTime : 1,
2✔
1052
        y0: isX ? 0 : startTime,
2✔
1053
        y1: isX ? 1 : endTime,
2✔
1054
        fillcolor: 'rgba(187, 196, 198, 0.4)',
1055
        line: { width: 0 },
1056
        layer: 'below',
1057
        ...fill
1058
    };
1059
};
1060

1061
const createLineShape = (axisId, axisType, time, line = {}) => {
1!
1062
    const isX = axisType === 'x';
4✔
1063
    return {
4✔
1064
        type: 'line',
1065
        xref: isX ? axisId : 'paper',
4✔
1066
        yref: isX ? 'paper' : axisId,
4✔
1067
        x0: isX ? time : 0,
4✔
1068
        x1: isX ? time : 1,
4✔
1069
        y0: isX ? 0 : time,
4✔
1070
        y1: isX ? 1 : time,
4✔
1071
        layer: 'above',
1072
        line: {
1073
            color: 'rgb(55, 128, 191)',
1074
            width: 3,
1075
            ...line
1076
        }
1077
    };
1078
};
1079

1080
export const DEFAULT_CURRENT_TIME_SHAPE_STYLE = [
1✔
1081
    "solid",
1082
    "dot",
1083
    "dash",
1084
    "longdash",
1085
    "dashdot",
1086
    "longdashdot"
1087
];
1088
export const DEFAULT_CURRENT_TIME_SHAPE_VALUES = {
1✔
1089
    color: 'rgba(58, 186, 111, 0.75)',
1090
    size: 3,
1091
    style: DEFAULT_CURRENT_TIME_SHAPE_STYLE[2]
1092
};
1093

1094
const addAxisShapes = (axisOpts, axisType, times) => {
1✔
1095
    const shapes = [];
6✔
1096
    const { startTime, endTime, hasBothDates } = times;
6✔
1097

1098
    axisOpts.forEach((axis, index) => {
6✔
1099
        if (axis.type === 'date' && axis.showCurrentTime === true) {
6!
1100
            const axisId = index === 0 ? axisType : `${axisType}${index + 1}`;
6!
1101
            if (hasBothDates) {
6✔
1102
                shapes.push(createRectShape(axisId, axisType, startTime, endTime, {
2✔
1103
                    fillcolor: axis.currentTimeShape?.color || DEFAULT_CURRENT_TIME_SHAPE_VALUES.color
4✔
1104
                }));
1105
            } else {
1106
                // Single dashed line
1107
                shapes.push(createLineShape(axisId, axisType, startTime, {
4✔
1108
                    color: axis.currentTimeShape?.color || DEFAULT_CURRENT_TIME_SHAPE_VALUES.color,
6✔
1109
                    dash: axis.currentTimeShape?.style || DEFAULT_CURRENT_TIME_SHAPE_VALUES.style,
6✔
1110
                    width: axis.currentTimeShape?.size || DEFAULT_CURRENT_TIME_SHAPE_VALUES.size
6✔
1111
                }));
1112
            }
1113
        }
1114
    });
1115

1116
    return shapes;
6✔
1117
};
1118

1119
/**
1120
 * Adds shapes representing the current time range to x or y axes of the selected chart.
1121
 *
1122
 * @param {Object} data - The data object containing chart information.
1123
 * @param {Array<Object>} [data.xAxisOpts] - The options for the x-axis, which may include properties like `type`, `showCurrentTime`, etc.
1124
 * @param {string|number} [data.yAxisOpts] - The options for the y-axis, which may include properties like `type`, `showCurrentTime`, etc.
1125
 * @param {Object} timeRange - The time range to visualize.
1126
 * @param {string|Date} [timeRange.start] - The start time of the range.
1127
 * @param {string|Date} [timeRange.end] - The end time of the range.
1128
 * @returns {Array<Object>} Array of shape objects for the current time range on both axes.
1129
 */
1130
export const addCurrentTimeShapes = (data, timeRange) => {
1✔
1131
    if (!timeRange.start && !timeRange.end) return [];
4✔
1132
    const xAxisOpts = data.xAxisOpts || [];
3!
1133
    const yAxisOpts = data.yAxisOpts || [];
3!
1134

1135
    // Split the time range
1136
    const startTime = timeRange.start;
3✔
1137
    const endTime = timeRange.end;
3✔
1138
    const hasBothDates = startTime && endTime;
3✔
1139

1140
    const times = { startTime, endTime, hasBothDates };
3✔
1141

1142
    // Create shapes for both x and y axes
1143
    const xAxisShapes = addAxisShapes(xAxisOpts, 'x', times);
3✔
1144
    const yAxisShapes = addAxisShapes(yAxisOpts, 'y', times);
3✔
1145

1146
    return [...xAxisShapes, ...yAxisShapes];
3✔
1147
};
1148

1149
/**
1150
 * Returns the default placeholder for Null value based on the data type.
1151
 * @param {string} type - The data type ('int', 'number', 'date', 'time', 'date-time', 'string', 'boolean')
1152
 * @returns {number|string} The default placeholder value for the given type
1153
 */
1154
export const getDefaultNullPlaceholderForDataType = (type) => {
1✔
1155
    switch (type) {
10✔
1156
    case 'int':
1157
    case 'number':
1158
        return 0;
2✔
1159
    case 'date':
1160
        return moment().format(dateFormats.date); // e.g., "2025-10-21Z"
1✔
1161
    case 'time':
1162
        return `1970-01-01T${moment().format(dateFormats.time)}`; // e.g., "1970-01-01T14:30:45Z"
1✔
1163
    case 'date-time':
1164
        return moment().format(dateFormats['date-time']); // e.g., "2025-10-21T14:30:45Z"
1✔
1165
    case 'string':
1166
    case 'boolean':
1167
    default:
1168
        return "NULL";
5✔
1169
    }
1170
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc