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

geosolutions-it / MapStore2 / 16448104046

22 Jul 2025 03:02PM UTC coverage: 76.93% (+0.004%) from 76.926%
16448104046

Pull #11348

github

web-flow
Merge c1d2b8bc0 into 659fb0e8c
Pull Request #11348: Extend the charts to show the current time

31360 of 48777 branches covered (64.29%)

37 of 52 new or added lines in 5 files covered. (71.15%)

7 existing lines in 2 files now uncovered.

38875 of 50533 relevant lines covered (76.93%)

36.53 hits per line

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

58.82
/web/client/components/widgets/builder/wizard/chart/ChartAxisOptions.jsx
1
/*
2
 * Copyright 2020, 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
import React, { useState } from 'react';
9
import { isNil, castArray } from 'lodash';
10
import uuidv1 from "uuid/v1";
11
import Select from 'react-select';
12
import { FormGroup, Radio, ControlLabel, InputGroup, Checkbox, Button as ButtonRB, Glyphicon, FormControl } from 'react-bootstrap';
13

14
import ChartValueFormatting from './ChartValueFormatting';
15
import Message from '../../../../I18N/Message';
16
import Font from '../common/Font';
17
import InfoPopover from '../../../widget/InfoPopover';
18
import tooltip from '../../../../misc/enhancers/tooltip';
19
import localizedProps from '../../../../misc/enhancers/localizedProps';
20
import DebouncedFormControl from '../../../../misc/DebouncedFormControl';
21
import { FONT } from '../../../../../utils/WidgetsUtils';
22
import ShapeStyle from './ShapeStyle';
23

24
const Button = tooltip(ButtonRB);
1✔
25
const AxisTypeSelect = localizedProps('options')(Select);
1✔
26

27
const AXIS_TYPES = [{
1✔
28
    value: '-',
29
    label: 'widgets.advanced.axisTypes.auto'
30
}, {
31
    value: 'linear',
32
    label: 'widgets.advanced.axisTypes.linear'
33
}, {
34
    value: 'category',
35
    label: 'widgets.advanced.axisTypes.category'
36
}, {
37
    value: 'log',
38
    label: 'widgets.advanced.axisTypes.log'
39
}, {
40
    value: 'date',
41
    label: 'widgets.advanced.axisTypes.date'
42
}];
43

44
const MAX_X_AXIS_LABELS = 200;
1✔
45
const getSelectedAxisId = ({
1✔
46
    axisKey,
47
    chart = {}
2✔
48
}) => {
49
    const axisOpts = castArray(chart?.[`${axisKey}AxisOpts`] || { id: 0 });
6✔
50
    const selectedAxisId = chart[`${axisKey}axis`] || 0;
6✔
51
    return axisOpts.some(opts => opts.id === selectedAxisId) ? selectedAxisId : 0;
6!
52
};
53

54
const AxisSelector = ({
1✔
55
    chart,
56
    onChange = () => {},
4✔
57
    onSelect = () => {},
×
58
    axisKey = 'x',
×
59
    selectedAxisId,
60
    defaultAddOptions
61
}) => {
62
    const [editTitle, setEditTitle] = useState(false);
6✔
63
    const axisOptsKey = `${axisKey}AxisOpts`;
6✔
64
    const traceAxisKey = `${axisKey}axis`;
6✔
65
    const axisOpts = castArray(chart?.[axisOptsKey] || { id: 0 });
6✔
66
    const options = axisOpts.find(({ id }) => id === selectedAxisId) || {};
6!
67
    return (
6✔
68
        <FormGroup className="form-group-flex">
69
            <InputGroup>
70
                {editTitle
6!
71
                    ? <DebouncedFormControl
72
                        value={options?.title || ''}
×
73
                        onChange={(value) => {
74
                            const newOptions = axisOpts
×
75
                                .map((axis) => axis.id === selectedAxisId ? { ...axis, title: value } : axis);
×
76
                            onChange(`charts[${chart?.chartId}].${axisOptsKey}`, newOptions);
×
77
                        }}
78
                    />
79
                    : <Select
80
                        clearable={false}
81
                        disabled={axisOpts.length === 1}
82
                        value={selectedAxisId}
83
                        options={axisOpts.map((axisOptions, idx) => ({
6✔
84
                            value: axisOptions.id,
85
                            label: `[ ${axisKey.toUpperCase()} ${idx} ] ${axisOptions.title || ''}`
12✔
86
                        }))}
87
                        onChange={(option) => {
88
                            onSelect(traceAxisKey, option?.value);
×
89
                        }}
90
                    />}
91
                <InputGroup.Button>
92
                    <Button
93
                        bsStyle="primary"
94
                        onClick={() => setEditTitle(!editTitle)}
×
95
                        tooltipId="widgets.builder.editAxisTitle"
96
                    >
97
                        <Glyphicon glyph={editTitle ? 'ok' : 'pencil'}/>
6!
98
                    </Button>
99
                </InputGroup.Button>
100
                <InputGroup.Button>
101
                    <Button
102
                        bsStyle="primary"
103
                        disabled={axisOpts.length >= chart?.traces?.length}
104
                        tooltipId="widgets.builder.addNewAxis"
105
                        onClick={() => {
106
                            const newAxis = {
×
107
                                ...defaultAddOptions,
108
                                id: uuidv1()
109
                            };
110
                            onChange(`charts[${chart?.chartId}].${axisOptsKey}`, [...axisOpts, newAxis]);
×
111
                            onSelect(traceAxisKey, newAxis.id);
×
112
                        }}
113
                    >
114
                        <Glyphicon glyph="plus"/>
115
                    </Button>
116
                </InputGroup.Button>
117
                <InputGroup.Button>
118
                    <Button
119
                        bsStyle="primary"
120
                        disabled={selectedAxisId === 0}
121
                        tooltipId="widgets.builder.removeAxis"
122
                        onClick={() => {
123
                            const newOptions = axisOpts.filter((axis) => axis.id !== selectedAxisId);
×
124
                            onChange(`charts[${chart?.chartId}].${axisOptsKey}`, newOptions);
×
125
                            onSelect(traceAxisKey, 0);
×
126
                        }}
127
                    >
128
                        <Glyphicon glyph="trash"/>
129
                    </Button>
130
                </InputGroup.Button>
131
            </InputGroup>
132
        </FormGroup>
133
    );
134
};
135

136
function AxisOptions({
137
    chart,
138
    chartPath,
139
    onChange,
140
    axisKey = 'y',
×
141
    sides = [{ value: 'left', labelId: 'widgets.advanced.left' }, { value: 'right', labelId: 'widgets.advanced.right' }],
×
142
    anchors = [{ value: 'y', label: 'Y' }, { value: 'free', labelId: 'widgets.advanced.free' }],
×
143
    hideForceTicksOption,
144
    hideValueFormatting,
145
    defaultAddOptions
146
}) {
147
    const axisOptsKey = `${axisKey}AxisOpts`;
6✔
148
    const axisOpts = castArray(chart?.[axisOptsKey] || { id: 0 });
6✔
149
    const selectedAxisId = getSelectedAxisId({
6✔
150
        axisKey,
151
        chart
152
    });
153
    const options = axisOpts.find(({ id }) => id === selectedAxisId) || {};
6!
154
    function handleChange(key, value) {
155
        const newOptions = axisOpts
1✔
156
            .map((axis) => axis.id === selectedAxisId ? { ...axis, [key]: value } : axis);
1!
157
        onChange(`${chartPath}.${axisOptsKey}`, newOptions);
1✔
158
    }
159
    return (
6✔
160
        <>
161
            <div className="ms-wizard-form-separator">
162
                <Message msgId={`widgets.advanced.${axisKey}Axis`} />
163
            </div>
164
            <AxisSelector
165
                axisKey={axisKey}
166
                chart={chart}
167
                selectedAxisId={selectedAxisId}
168
                defaultAddOptions={defaultAddOptions}
169
                onChange={onChange}
170
                onSelect={(key, value) => {
171
                    onChange(`${chartPath}.${key}`, value);
×
172
                }}
173
            />
174
            <FormGroup className="form-group-flex">
175
                <ControlLabel>
176
                    <Message msgId={`widgets.advanced.${axisKey}AxisType`} />
177
                </ControlLabel>
178
                <InputGroup>
179
                    <AxisTypeSelect
180
                        value={options?.type || '-'}
11✔
181
                        disabled={!!options.hide}
182
                        options={AXIS_TYPES}
183
                        clearable={false}
184
                        onChange={(option) => {
185
                            handleChange('type', option?.value);
×
186
                        }}
187
                    />
188
                </InputGroup>
189
            </FormGroup>
190
            <Font
191
                color={options?.color || chart?.layout?.color || FONT.COLOR}
18✔
192
                fontSize={options?.fontSize || chart?.layout?.fontSize || FONT.SIZE}
18✔
193
                fontFamily={options?.fontFamily || chart?.layout?.fontFamily || FONT.FAMILY}
18✔
194
                disabled={!!options.hide}
195
                onChange={handleChange}
196
            />
197
            {!hideValueFormatting && <ChartValueFormatting
9✔
198
                options={options}
199
                hideFormula
200
                onChange={handleChange}
201
            />}
202
            <FormGroup className="form-group-flex">
203
                <ControlLabel>
204
                    <Message msgId="widgets.advanced.side" />
205
                </ControlLabel>
206
                <InputGroup>
207
                    {sides.map(side => (
208
                        <Radio
12✔
209
                            key={side.value}
210
                            disabled={!!options.hide}
211
                            name={`${axisKey}-axis-side`}
212
                            value={side.value}
213
                            checked={(options.side || sides[0].value) === side.value}
24✔
214
                            onChange={(event) => {
215
                                handleChange('side', event?.target?.value);
×
216
                            }}
217
                            inline>
218
                            {side.labelId ? <Message msgId={side.labelId}/> : side.label}
12!
219
                        </Radio>
220
                    ))}
221
                </InputGroup>
222
            </FormGroup>
223
            <FormGroup className="form-group-flex">
224
                <ControlLabel>
225
                    <Message msgId="widgets.advanced.anchor" />
226
                </ControlLabel>
227
                <InputGroup style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
228
                    {anchors.map(side => (
229
                        <Radio
12✔
230
                            disabled={!!options.hide}
231
                            name={`${axisKey}-axis-anchor`}
232
                            key={side.value}
233
                            value={side.value}
234
                            checked={(options.anchor || anchors[0].value) === side.value}
24✔
235
                            onChange={(event) => {
236
                                handleChange('anchor', event?.target?.value);
×
237
                            }}
238
                            inline>
239
                            {side.labelId ? <Message msgId={side.labelId}/> : side.label}
12✔
240
                        </Radio>
241
                    ))}
242
                    <DebouncedFormControl
243
                        type="number"
244
                        disabled={options.anchor !== 'free' || !!options.hide}
6!
245
                        value={options.positionPx || 0}
12✔
246
                        min={0}
247
                        step={1}
248
                        fallbackValue={0}
249
                        style={{ maxWidth: 65, marginLeft: 'auto', zIndex: 0 }}
250
                        onChange={(value) => {
251
                            handleChange('positionPx', value);
×
252
                        }}
253
                    />
254
                    <InputGroup.Addon style={{ width: 'auto', lineHeight: 'normal' }}>
255
                        px
256
                    </InputGroup.Addon>
257
                </InputGroup>
258
            </FormGroup>
259
            {!hideForceTicksOption && <FormGroup className="form-group-flex" style={{ marginBottom: 0 }}>
9✔
260
                <Checkbox
261
                    disabled={options?.hide ?? false}
6✔
262
                    checked={!!options?.nTicks}
263
                    onChange={(event) => { handleChange('nTicks', event?.target?.checked ? MAX_X_AXIS_LABELS : undefined); }}
×
264
                >
265
                    <Message msgId="widgets.advanced.forceTicks" /> {' '}
266
                    {!(options?.hide ?? false) && <InfoPopover bsStyle="info" text={<Message msgId="widgets.advanced.maxXAxisLabels" msgParams={{ max: MAX_X_AXIS_LABELS }} />} />}
12✔
267
                </Checkbox>
268
            </FormGroup>}
269
            <FormGroup className="form-group-flex" style={{ marginBottom: 0 }}>
270
                <Checkbox
271
                    disabled={options?.hide ?? false}
12✔
272
                    checked={options.angle !== undefined}
273
                    onChange={(event) => { handleChange('angle', !event?.target?.checked ? undefined : 0); }}
×
274
                    style={{ flex: 'unset', marginRight: 8 }}
275
                >
276
                    <Message msgId="widgets.advanced.xAxisAngle" />
277
                </Checkbox>
278
                <InputGroup style={{ maxWidth: 80 }}>
279
                    {options.angle !== undefined
6!
280
                        ? <DebouncedFormControl
281
                            type="number"
282
                            min={-90}
283
                            max={90}
284
                            fallbackValue={0}
285
                            disabled={!!options?.hide}
286
                            value={!isNil(options.angle) ? options.angle : 0}
×
287
                            onChange={(value) => handleChange('angle', parseInt(value || 0, 10))}
×
288
                        />
289
                        : <FormControl disabled value={'Auto'} />}
290
                    <InputGroup.Addon>°</InputGroup.Addon>
291
                </InputGroup>
292
            </FormGroup>
293
            <FormGroup className="form-group-flex" style={{ marginBottom: 0 }}>
294
                <Checkbox
295
                    checked={options?.hide ?? false}
12✔
296
                    onChange={(event) => { handleChange('hide', event?.target?.checked); }}
1✔
297
                >
298
                    <Message msgId="widgets.advanced.hideLabels" />
299
                </Checkbox>
300
            </FormGroup>
301
            {options.type === 'date' && <FormGroup className="form-group-flex">
7✔
302
                <Checkbox
303
                    checked={options?.showCurrentTime ?? false}
2✔
NEW
304
                    onChange={(event) => { handleChange('showCurrentTime', event?.target?.checked); }}
×
305
                >
306
                    <Message msgId="widgets.advanced.showCurrentTime" />
307
                </Checkbox>
308
            </FormGroup>}
309
            {options.type === 'date' && options.showCurrentTime && (
7!
310
                <ShapeStyle
311
                    color={options?.currentTimeShape?.color}
312
                    size={options?.currentTimeShape?.size}
313
                    style={options?.currentTimeShape?.style}
NEW
314
                    onChange={(key, value) => handleChange('currentTimeShape', { ...options.currentTimeShape, [key]: value })}
×
315
                />
316
            )}
317
        </>
318
    );
319
}
320
/**
321
 * ChartAxisOptions. A component that renders fields to change the chart x/y axis options
322
 * @prop {object} data the widget chart data
323
 * @prop {function} onChange callback on every input change
324
 */
325
function ChartAxisOptions({
326
    data = {},
1✔
327
    onChange
328
}) {
329
    const selectedChart = (data?.charts || []).find((chart) => chart.chartId === data.selectedChartId);
3✔
330
    const chartPath = `charts[${selectedChart?.chartId}]`;
3✔
331
    return (
3✔
332
        <>
333
            <AxisOptions
334
                key={`y-axis-${selectedChart?.chartId}`}
335
                chart={selectedChart}
336
                chartPath={chartPath}
337
                onChange={onChange}
338
                axisKey="y"
339
                sides={[{ value: 'left', labelId: 'widgets.advanced.left' }, { value: 'right', labelId: 'widgets.advanced.right' }]}
340
                anchors={[{ value: 'x', label: 'X' }, { value: 'free', labelId: 'widgets.advanced.free' }]}
341
                hideForceTicksOption
342
                hideValueFormatting={false}
343
                defaultAddOptions={{
344
                    side: 'right'
345
                }}
346
            />
347
            <AxisOptions
348
                key={`x-axis-${selectedChart?.chartId}`}
349
                chart={selectedChart}
350
                chartPath={chartPath}
351
                onChange={onChange}
352
                axisKey="x"
353
                sides={[{ value: 'bottom', labelId: 'widgets.advanced.bottom' }, { value: 'top', labelId: 'widgets.advanced.top' }]}
354
                anchors={[{ value: 'y', label: 'Y' }, { value: 'free', labelId: 'widgets.advanced.free' }]}
355
                hideForceTicksOption={false}
356
                hideValueFormatting
357
            />
358
        </>
359
    );
360
}
361

362
export default ChartAxisOptions;
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