@c8y/tutorial 1023.53.0 → 1023.57.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.
@@ -892,6 +892,13 @@ export default {
892
892
  description: 'This is an example for the countdown component.',
893
893
  scope: 'self'
894
894
  },
895
+ {
896
+ name: 'Charts',
897
+ module: 'ChartsExampleModule',
898
+ path: './src/c8y-charts/charts-example.module.ts',
899
+ description: 'This is an example for the charts component.',
900
+ scope: 'self'
901
+ },
895
902
  {
896
903
  name: 'Bottom drawer',
897
904
  module: 'bottomDrawerExampleModuleProviders',
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@c8y/tutorial",
3
- "version": "1023.53.0",
3
+ "version": "1023.57.0",
4
4
  "description": "This package is used to scaffold a tutorial for Cumulocity IoT Web SDK which explains a lot of concepts.",
5
5
  "dependencies": {
6
- "@c8y/style": "1023.53.0",
7
- "@c8y/ngx-components": "1023.53.0",
8
- "@c8y/client": "1023.53.0",
9
- "@c8y/bootstrap": "1023.53.0",
6
+ "@c8y/style": "1023.57.0",
7
+ "@c8y/ngx-components": "1023.57.0",
8
+ "@c8y/client": "1023.57.0",
9
+ "@c8y/bootstrap": "1023.57.0",
10
10
  "@angular/cdk": "^20.2.14",
11
11
  "monaco-editor": "~0.53.0",
12
12
  "ngx-bootstrap": "20.0.2",
@@ -14,8 +14,8 @@
14
14
  "rxjs": "7.8.2"
15
15
  },
16
16
  "devDependencies": {
17
- "@c8y/options": "1023.53.0",
18
- "@c8y/devkit": "1023.53.0"
17
+ "@c8y/options": "1023.57.0",
18
+ "@c8y/devkit": "1023.57.0"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@angular/common": ">=20 <21"
@@ -21,6 +21,10 @@ export class MeasurementsInterceptor implements HttpInterceptor {
21
21
  }
22
22
 
23
23
  private mockGET(_requestDescriptor: string) {
24
+ if (_requestDescriptor.includes('measurement/measurements/series')) {
25
+ return this.mockSeriesGET(_requestDescriptor);
26
+ }
27
+
24
28
  const responseGenerators = this.getResponseGenerators();
25
29
 
26
30
  for (const urlPart in responseGenerators) {
@@ -34,6 +38,72 @@ export class MeasurementsInterceptor implements HttpInterceptor {
34
38
  return null;
35
39
  }
36
40
 
41
+ private mockSeriesGET(_requestDescriptor: string) {
42
+ const jsonMatch = _requestDescriptor.match(/series(.*)$/);
43
+
44
+ if (!jsonMatch || !jsonMatch[1]) {
45
+ return null;
46
+ }
47
+
48
+ let params: any = {};
49
+ try {
50
+ params = JSON.parse(jsonMatch[1]);
51
+ } catch (e) {
52
+ console.error('Failed to parse series params', e);
53
+ return null;
54
+ }
55
+
56
+ const { dateFrom, dateTo, series } = params;
57
+
58
+ if (!dateFrom || !dateTo) {
59
+ return null;
60
+ }
61
+
62
+ const start = new Date(dateFrom);
63
+ const end = new Date(dateTo);
64
+ const values: Record<string, { min: number; max: number }[]> = {};
65
+
66
+ const current = new Date(start);
67
+ current.setMinutes(0, 0, 0);
68
+
69
+ while (current <= end) {
70
+ const timestamp = current.toISOString();
71
+
72
+ const baseValue = 50 + Math.sin(current.getTime() / (1000 * 60 * 60 * 6)) * 30;
73
+ const variation = Math.random() * 20 - 10;
74
+ const value = Math.max(0, Math.min(100, baseValue + variation));
75
+
76
+ values[timestamp] = [
77
+ {
78
+ min: Number(value.toFixed(2)),
79
+ max: Number(value.toFixed(2))
80
+ }
81
+ ];
82
+
83
+ current.setHours(current.getHours() + 1);
84
+ }
85
+
86
+ let seriesInfo = {
87
+ unit: '%',
88
+ name: 'Battery',
89
+ type: 'c8y_Battery'
90
+ };
91
+
92
+ if (series?.includes('c8y_Temperature')) {
93
+ seriesInfo = {
94
+ unit: '°C',
95
+ name: 'T',
96
+ type: 'c8y_Temperature'
97
+ };
98
+ }
99
+
100
+ return generateResponse(() => ({
101
+ values,
102
+ series: [seriesInfo],
103
+ truncated: false
104
+ }));
105
+ }
106
+
37
107
  private getResponseGenerators() {
38
108
  return {
39
109
  c8y_Battery: () => ({
@@ -0,0 +1,206 @@
1
+ <div class="container-fluid">
2
+ <div class="row m-t-16">
3
+ <div class="col-lg-12">
4
+ <div class="card">
5
+ <div class="card-header">
6
+ <h3 class="card-title">Example Configurations</h3>
7
+ </div>
8
+ <div class="card-block">
9
+ <div
10
+ class="btn-group"
11
+ role="group"
12
+ >
13
+ @for (example of configExamples; track example.value) {
14
+ <button
15
+ class="btn"
16
+ [class.btn-primary]="selectedExample === example.value"
17
+ [class.btn-default]="selectedExample !== example.value"
18
+ type="button"
19
+ (click)="loadExampleConfiguration(example.value)"
20
+ >
21
+ {{ example.label }}
22
+ </button>
23
+ }
24
+ </div>
25
+
26
+ <div class="m-t-16">
27
+ @switch (selectedExample) {
28
+ @case ('basic') {
29
+ <div class="alert alert-info">
30
+ <strong>Basic Chart:</strong>
31
+ Simple chart with 2 datapoints showing temperature data over the last 24 hours.
32
+ Good starting point for understanding the component.
33
+ </div>
34
+ }
35
+ @case ('slider') {
36
+ <div class="alert alert-info">
37
+ <strong>With Slider:</strong>
38
+ Chart with navigation slider enabled for exploring 7 days of data. Supports data
39
+ aggregation and zoom capabilities.
40
+ </div>
41
+ }
42
+ @case ('multiaxis') {
43
+ <div class="alert alert-info">
44
+ <strong>Multiple Axes:</strong>
45
+ Chart displaying 4 datapoints with different units, each with its own Y-axis.
46
+ Shows how to handle heterogeneous data types.
47
+ </div>
48
+ }
49
+ }
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="row m-t-16">
57
+ <div class="col-lg-9">
58
+ <div class="card">
59
+ <div class="card-header">
60
+ <h3 class="card-title">Chart Visualization</h3>
61
+ <span class="card-header-actions">
62
+ <span class="badge">Active datapoints: {{ activeDatapoints.length }}</span>
63
+ </span>
64
+ </div>
65
+ <div
66
+ class="card-block"
67
+ style="height: 500px"
68
+ >
69
+ <c8y-charts
70
+ [config]="config"
71
+ [alerts]="alerts"
72
+ [chartViewContext]="chartViewContext"
73
+ (updateAlarmsAndEvents)="updateAlarmsAndEvents($event)"
74
+ (configChangeOnZoomOut)="onSliderZoom($event)"
75
+ (datapointOutOfSync)="handleDatapointOutOfSync($event)"
76
+ (timeRangeChangeOnRealtime)="updateTimeRangeOnRealtime($event)"
77
+ (isMarkedAreaEnabled)="onMarkedAreaEnabled($event)"
78
+ (updateActiveDatapoints)="updateActiveDatapoints($event)"
79
+ (updateAggregatedSliderDatapoint)="updateAggregatedSliderDatapoint($event)"
80
+ ></c8y-charts>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="col-lg-3">
86
+ <div class="card">
87
+ <div class="card-header">
88
+ <h3 class="card-title">Chart Options</h3>
89
+ </div>
90
+ <div class="card-block">
91
+ <fieldset class="c8y-fieldset">
92
+ <legend>Display Options</legend>
93
+
94
+ <label class="c8y-checkbox">
95
+ <input
96
+ type="checkbox"
97
+ [checked]="config.yAxisSplitLines"
98
+ (change)="toggleOption('yAxisSplitLines')"
99
+ />
100
+ <span></span>
101
+ <span>Y-axis helper lines</span>
102
+ </label>
103
+
104
+ <label class="c8y-checkbox">
105
+ <input
106
+ type="checkbox"
107
+ [checked]="config.xAxisSplitLines"
108
+ (change)="toggleOption('xAxisSplitLines')"
109
+ />
110
+ <span></span>
111
+ <span>X-axis helper lines</span>
112
+ </label>
113
+
114
+ <label class="c8y-checkbox">
115
+ <input
116
+ type="checkbox"
117
+ [checked]="config.showSlider"
118
+ (change)="toggleOption('showSlider')"
119
+ />
120
+ <span></span>
121
+ <span>Show slider</span>
122
+ </label>
123
+
124
+ <label class="c8y-checkbox">
125
+ <input
126
+ type="checkbox"
127
+ [checked]="config.smoothLines"
128
+ (change)="toggleOption('smoothLines')"
129
+ />
130
+ <span></span>
131
+ <span>Smooth lines</span>
132
+ </label>
133
+
134
+ <label class="c8y-checkbox">
135
+ <input
136
+ type="checkbox"
137
+ [checked]="config.showLabelAndUnit"
138
+ (change)="toggleOption('showLabelAndUnit')"
139
+ />
140
+ <span></span>
141
+ <span>Show labels & units</span>
142
+ </label>
143
+ </fieldset>
144
+
145
+ <fieldset class="c8y-fieldset m-t-16">
146
+ <legend>Axis Options</legend>
147
+
148
+ <label class="c8y-checkbox">
149
+ <input
150
+ type="checkbox"
151
+ [checked]="config.mergeMatchingDatapoints"
152
+ (change)="toggleOption('mergeMatchingDatapoints')"
153
+ />
154
+ <span></span>
155
+ <span>Merge matching datapoints</span>
156
+ </label>
157
+
158
+ <label class="c8y-checkbox">
159
+ <input
160
+ type="checkbox"
161
+ [checked]="config.forceMergeDatapoints"
162
+ (change)="toggleOption('forceMergeDatapoints')"
163
+ />
164
+ <span></span>
165
+ <span>Force merge all datapoints</span>
166
+ </label>
167
+
168
+ <label class="c8y-checkbox">
169
+ <input
170
+ type="checkbox"
171
+ [checked]="config.setYaxisStartToZero"
172
+ (change)="toggleOption('setYaxisStartToZero')"
173
+ />
174
+ <span></span>
175
+ <span>Y-axis starts at 0</span>
176
+ </label>
177
+ </fieldset>
178
+
179
+ <fieldset class="c8y-fieldset m-t-16">
180
+ <legend>Alarms & Events</legend>
181
+
182
+ <label class="c8y-checkbox">
183
+ <input
184
+ type="checkbox"
185
+ [checked]="config.displayMarkedLine"
186
+ (change)="toggleOption('displayMarkedLine')"
187
+ />
188
+ <span></span>
189
+ <span>Show vertical lines</span>
190
+ </label>
191
+
192
+ <label class="c8y-checkbox">
193
+ <input
194
+ type="checkbox"
195
+ [checked]="config.displayMarkedPoint"
196
+ (change)="toggleOption('displayMarkedPoint')"
197
+ />
198
+ <span></span>
199
+ <span>Show icons</span>
200
+ </label>
201
+ </fieldset>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
@@ -0,0 +1,379 @@
1
+ import { Component, OnInit, ViewChild } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import {
5
+ CoreModule,
6
+ DynamicComponentAlert,
7
+ DynamicComponentAlertAggregator,
8
+ WidgetTimeContextDateRangeService,
9
+ DateAndTimeOptions
10
+ } from '@c8y/ngx-components';
11
+ import { TimeContextComponent } from '@c8y/ngx-components/time-context';
12
+ import {
13
+ AlarmOrEventExtended,
14
+ CHART_VIEW_CONTEXT,
15
+ ChartAlarmsService,
16
+ ChartEventsService,
17
+ ChartHelpersService,
18
+ ChartsComponent,
19
+ DatapointsGraphKPIDetails,
20
+ DatapointsGraphWidgetConfig
21
+ } from '@c8y/ngx-components/echart';
22
+ import { aggregationType } from '@c8y/client';
23
+
24
+ /**
25
+ * This example demonstrates how to use the c8y-charts component to display
26
+ * time-series data with datapoints, alarms, and events.
27
+ *
28
+ * The c8y-charts component is a powerful visualization tool that:
29
+ * - Displays multiple datapoint series on a chart
30
+ * - Supports real-time data updates
31
+ * - Shows alarms and events as markers
32
+ * - Provides zoom and pan capabilities
33
+ * - Supports data aggregation and slider navigation
34
+ *
35
+ * Key features demonstrated:
36
+ * - Basic chart configuration
37
+ * - Handling chart events (zoom, datapoint updates)
38
+ * - Working with alerts and notifications
39
+ * - Chart view context management
40
+ */
41
+ @Component({
42
+ selector: 'app-charts-example',
43
+ templateUrl: './charts-example.component.html',
44
+ standalone: true,
45
+ imports: [CommonModule, CoreModule, FormsModule, ChartsComponent],
46
+ providers: [
47
+ ChartEventsService,
48
+ ChartAlarmsService,
49
+ ChartHelpersService,
50
+ WidgetTimeContextDateRangeService
51
+ ]
52
+ })
53
+ export class ChartsExampleComponent implements OnInit {
54
+ // Chart configuration object
55
+ config: DatapointsGraphWidgetConfig = {
56
+ datapoints: [],
57
+ alarmsEventsConfigs: [],
58
+ dateFrom: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 24 hours ago
59
+ dateTo: new Date().toISOString(),
60
+ interval: 'custom',
61
+ realtime: false,
62
+ yAxisSplitLines: true,
63
+ xAxisSplitLines: false,
64
+ showLabelAndUnit: true,
65
+ showSlider: true,
66
+ smoothLines: false,
67
+ displayMarkedLine: true,
68
+ displayMarkedPoint: true,
69
+ mergeMatchingDatapoints: false,
70
+ forceMergeDatapoints: false,
71
+ setYaxisStartToZero: false,
72
+ numberOfDecimalPlaces: 2
73
+ };
74
+
75
+ // Alert aggregator for chart notifications
76
+ alerts = new DynamicComponentAlertAggregator();
77
+
78
+ // Chart view context - determines the chart behavior and features
79
+ chartViewContext = CHART_VIEW_CONTEXT.DATAPOINT_EXPLORER;
80
+
81
+ // Active datapoints currently visible in the chart
82
+ activeDatapoints: DatapointsGraphKPIDetails[] = [];
83
+
84
+ // Time context reference
85
+ @ViewChild('timeContext') timeContext: TimeContextComponent;
86
+
87
+ // Time properties for binding to time context
88
+ timeProps: {
89
+ dateFrom: Date;
90
+ dateTo: Date;
91
+ interval?: string;
92
+ realtime?: boolean;
93
+ aggregation?: aggregationType | null;
94
+ };
95
+
96
+ // Time picker configuration
97
+ readonly TIME_PICKER_CONFIG: DateAndTimeOptions = {
98
+ showMinutes: true,
99
+ showSeconds: true,
100
+ showSpinners: false
101
+ };
102
+
103
+ // Flag to skip onTimeContextChange during zoom handling
104
+ private isHandlingZoom = false;
105
+
106
+ // Configuration examples
107
+ configExamples = [
108
+ { label: 'Basic Chart', value: 'basic' },
109
+ { label: 'With Slider', value: 'slider' },
110
+ { label: 'Multiple Axes', value: 'multiaxis' }
111
+ ];
112
+
113
+ selectedExample = 'basic';
114
+
115
+ ngOnInit(): void {
116
+ // Initialize with a basic configuration
117
+ this.loadExampleConfiguration('basic');
118
+
119
+ // Initialize timeProps from config
120
+ if (this.config.dateFrom && this.config.dateTo) {
121
+ this.timeProps = {
122
+ dateFrom: new Date(this.config.dateFrom),
123
+ dateTo: new Date(this.config.dateTo),
124
+ interval: this.config.interval as any,
125
+ realtime: this.config.realtime,
126
+ aggregation: this.config.realtime ? null : this.config.aggregation
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Load different example configurations to demonstrate various chart capabilities
133
+ */
134
+ loadExampleConfiguration(example: string): void {
135
+ this.selectedExample = example;
136
+
137
+ switch (example) {
138
+ case 'basic':
139
+ this.loadBasicExample();
140
+ break;
141
+ case 'realtime':
142
+ this.loadRealtimeExample();
143
+ break;
144
+ case 'slider':
145
+ this.loadSliderExample();
146
+ break;
147
+ case 'multiaxis':
148
+ this.loadMultiAxisExample();
149
+ break;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Handle zoom events from the chart
155
+ */
156
+ onSliderZoom(timeProps: { dateFrom: Date; dateTo: Date; interval?: 'hours' | 'weeks' }): void {
157
+ this.isHandlingZoom = true;
158
+
159
+ const dateFrom = timeProps.dateFrom.toISOString();
160
+ const dateTo = timeProps.dateTo.toISOString();
161
+ const interval = timeProps.interval || 'custom';
162
+
163
+ if (
164
+ this.config.dateFrom === dateFrom &&
165
+ this.config.dateTo === dateTo &&
166
+ this.config.interval === interval &&
167
+ this.config.realtime === false
168
+ ) {
169
+ this.isHandlingZoom = false;
170
+ return;
171
+ }
172
+
173
+ // Update config
174
+ this.config = {
175
+ ...this.config,
176
+ dateFrom,
177
+ dateTo,
178
+ interval,
179
+ realtime: false,
180
+ isAutoRefreshEnabled: false,
181
+ refreshInterval: 0
182
+ };
183
+
184
+ // Update timeProps binding
185
+ this.timeProps = {
186
+ dateFrom: new Date(timeProps.dateFrom.getTime()),
187
+ dateTo: new Date(timeProps.dateTo.getTime()),
188
+ interval: 'custom',
189
+ realtime: false
190
+ };
191
+
192
+ this.isHandlingZoom = false;
193
+ }
194
+
195
+ /**
196
+ * Handle real-time updates to the time range
197
+ */
198
+ updateTimeRangeOnRealtime(
199
+ timeRange: Pick<DatapointsGraphWidgetConfig, 'dateFrom' | 'dateTo'>
200
+ ): void {
201
+ if (this.config.dateFrom === timeRange.dateFrom && this.config.dateTo === timeRange.dateTo) {
202
+ return;
203
+ }
204
+
205
+ this.config = {
206
+ ...this.config,
207
+ ...timeRange
208
+ };
209
+
210
+ // Update timeProps for time context
211
+ if (timeRange.dateFrom && timeRange.dateTo) {
212
+ this.timeProps = {
213
+ ...this.timeProps,
214
+ dateFrom: new Date(timeRange.dateFrom),
215
+ dateTo: new Date(timeRange.dateTo)
216
+ };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Handle datapoint synchronization issues
222
+ */
223
+ handleDatapointOutOfSync(datapoint: DatapointsGraphKPIDetails): void {
224
+ console.warn('Datapoint out of sync:', datapoint);
225
+
226
+ const alert = new DynamicComponentAlert({
227
+ type: 'warning',
228
+ text: 'Datapoint out of sync: ' + datapoint.label
229
+ });
230
+
231
+ this.alerts?.addAlerts(alert);
232
+ }
233
+
234
+ /**
235
+ * Update alarms and events displayed on the chart
236
+ */
237
+ updateAlarmsAndEvents(alarmsEventsConfigs: AlarmOrEventExtended[]): void {
238
+ if (this.config.alarmsEventsConfigs === alarmsEventsConfigs) {
239
+ return;
240
+ }
241
+
242
+ this.config = {
243
+ ...this.config,
244
+ alarmsEventsConfigs
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Track which datapoints are currently active and visible
250
+ */
251
+ updateActiveDatapoints(activeDatapoints: DatapointsGraphKPIDetails[]): void {
252
+ this.activeDatapoints = activeDatapoints;
253
+ console.log('Active datapoints:', activeDatapoints.length);
254
+ }
255
+
256
+ /**
257
+ * Handle marked area state changes (when alarms are clicked)
258
+ */
259
+ onMarkedAreaEnabled(isEnabled: boolean): void {
260
+ console.log('Marked area enabled:', isEnabled);
261
+ }
262
+
263
+ /**
264
+ * Handle aggregated slider datapoint selection
265
+ */
266
+ updateAggregatedSliderDatapoint(selectedDatapoint: any): void {
267
+ console.log('Aggregated slider datapoint updated:', selectedDatapoint);
268
+ }
269
+
270
+ /**
271
+ * Toggle a configuration option
272
+ */
273
+ toggleOption(option: keyof DatapointsGraphWidgetConfig): void {
274
+ this.config = {
275
+ ...this.config,
276
+ [option]: !this.config[option]
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Basic chart example with simple configuration
282
+ */
283
+ private loadBasicExample(): void {
284
+ this.config = {
285
+ ...this.config,
286
+ datapoints: this.createSampleDatapoints(2),
287
+ dateFrom: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
288
+ dateTo: new Date().toISOString(),
289
+ interval: 'custom',
290
+ realtime: false,
291
+ showSlider: false,
292
+ yAxisSplitLines: true,
293
+ xAxisSplitLines: false
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Real-time chart example with auto-refresh
299
+ */
300
+ private loadRealtimeExample(): void {
301
+ this.config = {
302
+ ...this.config,
303
+ datapoints: this.createSampleDatapoints(1),
304
+ dateFrom: new Date(Date.now() - 60 * 60 * 1000).toISOString(), // 1 hour ago
305
+ dateTo: new Date().toISOString(),
306
+ interval: 'hours',
307
+ realtime: true,
308
+ isAutoRefreshEnabled: true,
309
+ refreshInterval: 5000,
310
+ showSlider: false
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Chart with slider for data exploration
316
+ */
317
+ private loadSliderExample(): void {
318
+ this.config = {
319
+ ...this.config,
320
+ datapoints: this.createSampleDatapoints(3),
321
+ dateFrom: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago
322
+ dateTo: new Date().toISOString(),
323
+ interval: 'weeks',
324
+ realtime: false,
325
+ showSlider: true,
326
+ aggregation: aggregationType.HOURLY
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Chart with multiple Y-axes for different data types
332
+ */
333
+ private loadMultiAxisExample(): void {
334
+ this.config = {
335
+ ...this.config,
336
+ datapoints: this.createSampleDatapoints(4, true),
337
+ dateFrom: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
338
+ dateTo: new Date().toISOString(),
339
+ interval: 'custom',
340
+ realtime: false,
341
+ showSlider: true,
342
+ mergeMatchingDatapoints: false,
343
+ showLabelAndUnit: true
344
+ };
345
+ }
346
+
347
+ /**
348
+ * Create sample datapoints for demonstration
349
+ * In a real application, these would come from your device/asset data
350
+ */
351
+ private createSampleDatapoints(count: number, variedUnits = false): DatapointsGraphKPIDetails[] {
352
+ const datapoints: DatapointsGraphKPIDetails[] = [];
353
+ const units = variedUnits ? ['°C', '%', 'kWh', 'bar'] : ['°C'];
354
+
355
+ for (let i = 0; i < count; i++) {
356
+ datapoints.push({
357
+ __target: { id: `device${i + 1}`, name: `Device ${i + 1}` },
358
+ fragment: 'c8y_Battery',
359
+ series: '%',
360
+ label: `Temperature ${i + 1}`,
361
+ unit: units[i % units.length],
362
+ color: this.getColorForIndex(i),
363
+ lineType: 'line',
364
+ renderType: 'min',
365
+ __active: true
366
+ } as any);
367
+ }
368
+
369
+ return datapoints;
370
+ }
371
+
372
+ /**
373
+ * Get a color for a datapoint based on its index
374
+ */
375
+ private getColorForIndex(index: number): string {
376
+ const colors = ['#1776BF', '#14A68E', '#F3B511', '#E66C37', '#9D5BBA'];
377
+ return colors[index % colors.length];
378
+ }
379
+ }
@@ -0,0 +1,20 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { NavigatorNode, hookNavigator, hookRoute } from '@c8y/ngx-components';
3
+
4
+ @NgModule({
5
+ providers: [
6
+ hookRoute({
7
+ path: 'chart-example',
8
+ loadComponent: () => import('./charts-example.component').then(m => m.ChartsExampleComponent)
9
+ }),
10
+ hookNavigator(
11
+ new NavigatorNode({
12
+ path: '/chart-example',
13
+ label: 'Chart Example',
14
+ icon: 'combo-chart',
15
+ priority: 115
16
+ })
17
+ )
18
+ ]
19
+ })
20
+ export class ChartsExampleModule {}
@@ -14,6 +14,8 @@ export interface ICounterService {
14
14
  count: () => void;
15
15
  }
16
16
 
17
+ export {};
18
+
17
19
  declare global {
18
20
  /**
19
21
  * The `CumulocityServiceRegistry` namespaces is declared in `@c8y/ngx-components` as part of the global scope.
@@ -17,6 +17,11 @@ import { DeleteModalExampleComponent } from './delete-modal-example.component';
17
17
  Confirm modal using <code>c8y-confirm-modal</code> component
18
18
  </button>
19
19
  </div>
20
+ <div class="p-b-24 text-center">
21
+ <button class="btn btn-default" (click)="deleteTenantWithCodeVerification()">
22
+ Confirm modal with code verification
23
+ </button>
24
+ </div>
20
25
  </div>`,
21
26
  standalone: true,
22
27
  imports: [ModalModule, FormsModule, CoreModule]
@@ -69,4 +74,25 @@ export class ConfirmModalExampleComponent {
69
74
  console.log('Cancel clicked');
70
75
  }
71
76
  }
77
+
78
+ async deleteTenantWithCodeVerification() {
79
+ try {
80
+ await this.modalService.confirm(
81
+ 'Confirm delete?',
82
+ 'You are about to delete the tenant. This action will immediately affect all users.<br><br>For security reasons, enter the following code to continue:',
83
+ Status.DANGER,
84
+ {
85
+ ok: 'Delete'
86
+ },
87
+ {},
88
+ undefined,
89
+ true // requireCodeVerification
90
+ );
91
+ // eslint-disable-next-line no-console
92
+ console.log('Delete confirmed with code verification');
93
+ } catch (e) {
94
+ // eslint-disable-next-line no-console
95
+ console.log('Cancel clicked or code verification failed');
96
+ }
97
+ }
72
98
  }