cats-charts 0.0.52 → 0.0.54

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.
@@ -31,16 +31,232 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
31
31
  class Charts {
32
32
  legendSelected = {};
33
33
  defaultColors = ['#22c55e', '#3b82f6', '#ef4444', '#6b7280'];
34
+ // buildGraphicLegend(total: number | null, config: any, chartInstance: any, chartType?: string) {
35
+ // const pillHeight = 28;
36
+ // const spacing = 14;
37
+ // const indicatorWidth = 12;
38
+ // const padding = 8;
39
+ // const font = '14px sans-serif';
40
+ // const containerWidth = chartInstance?.getWidth?.() ?? 800;
41
+ // const maxWidth = containerWidth * (config?.legendContainerWidth ?? 0.7);
42
+ // const selectedMap = chartInstance?.getOption()?.legend?.[0]?.selected || {};
43
+ // let colorIndex = 0;
44
+ // const pills = (
45
+ // chartType === 'line' || chartType === 'bar' || chartType === 'scatter'
46
+ // ? config?.series
47
+ // : config?.data
48
+ // )?.map((item: any) => {
49
+ // const hasCustomColors = Array.isArray(config?.colors) && config?.colors?.length > 0;
50
+ // const color =
51
+ // total === 0
52
+ // ? '#E6E7E8'
53
+ // : (item?.itemStyle?.color ??
54
+ // (hasCustomColors
55
+ // ? config?.colors[colorIndex % config?.colors?.length]
56
+ // : this.defaultColors[colorIndex % this.defaultColors.length]));
57
+ // if (
58
+ // !item?.itemStyle?.color ||
59
+ // (item?.itemStyle?.color && Object.keys(selectedMap).length && !selectedMap[item.name])
60
+ // ) {
61
+ // colorIndex += 1;
62
+ // }
63
+ // const nameRect = echarts.format.getTextRect(item.name, font);
64
+ // const valueRect =
65
+ // chartType === 'line' || chartType === 'bar' || chartType === 'scatter'
66
+ // ? { width: 0 }
67
+ // : echarts.format.getTextRect(
68
+ // String(config?.valueFormatter ? config.valueFormatter(item.value) : item.value),
69
+ // font,
70
+ // );
71
+ // const pillWidth =
72
+ // padding +
73
+ // indicatorWidth +
74
+ // 6 +
75
+ // nameRect.width +
76
+ // (chartType === 'line' || chartType === 'bar' || chartType === 'scatter'
77
+ // ? 0
78
+ // : 8 + valueRect.width) +
79
+ // padding;
80
+ // return { item, color, pillWidth };
81
+ // });
82
+ // // 🔹 Build rows dynamically
83
+ // const rows: any[] = [];
84
+ // let currentRow: any[] = [];
85
+ // let rowWidth = 0;
86
+ // pills?.forEach((pill: any) => {
87
+ // if (rowWidth + pill.pillWidth > maxWidth && currentRow.length) {
88
+ // rows.push({ pills: currentRow, width: rowWidth });
89
+ // currentRow = [];
90
+ // rowWidth = 0;
91
+ // }
92
+ // currentRow.push(pill);
93
+ // rowWidth += pill.pillWidth + spacing;
94
+ // });
95
+ // if (currentRow.length) rows.push({ pills: currentRow, width: rowWidth });
96
+ // // 🔹 Build graphic children
97
+ // let yOffset = 0;
98
+ // const children: any[] = [];
99
+ // rows?.forEach((row) => {
100
+ // let xOffset =
101
+ // config?.legendAlign === 'center' && config?.legendDirection !== 'vertical'
102
+ // ? (maxWidth - row.width) / 2
103
+ // : maxWidth;
104
+ // row?.pills?.forEach((pill: any) => {
105
+ // const isSelected = selectedMap[pill.item.name] !== false;
106
+ // const isDisabled = pill.item.value === 0;
107
+ // children.push({
108
+ // type: 'group',
109
+ // left: xOffset,
110
+ // top: yOffset,
111
+ // width: pill.pillWidth,
112
+ // height: pillHeight,
113
+ // cursor: isDisabled ? 'default' : 'pointer',
114
+ // onclick: () => {
115
+ // if (isDisabled) return;
116
+ // chartInstance.dispatchAction({
117
+ // type: 'legendToggleSelect',
118
+ // name: pill.item.name,
119
+ // });
120
+ // chartInstance.setOption({
121
+ // graphic: this.buildGraphicLegend(total, config, chartInstance, chartType),
122
+ // });
123
+ // },
124
+ // children: [
125
+ // {
126
+ // type: 'rect',
127
+ // shape: {
128
+ // width: pill.pillWidth,
129
+ // height: pillHeight,
130
+ // r: 4,
131
+ // },
132
+ // style: {
133
+ // fill:
134
+ // isSelected && !isDisabled
135
+ // ? this.hexToRgba(pill.color, total === 0 ? 0.3 : 0.03)
136
+ // : '#F2F2F7',
137
+ // stroke: isSelected && !isDisabled ? this.hexToRgba(pill.color, 0.3) : '#D1D1D6',
138
+ // lineWidth: 1,
139
+ // },
140
+ // },
141
+ // {
142
+ // type: 'rect',
143
+ // shape: { width: 12, height: config?.legendIndicatorHeight ?? 12, r: 2 },
144
+ // left: padding,
145
+ // top: (pillHeight - (config?.legendIndicatorHeight ?? 12)) / 2,
146
+ // style: {
147
+ // fill: isSelected && !isDisabled ? pill.color : '#C7C7CC',
148
+ // },
149
+ // },
150
+ // {
151
+ // type: 'text',
152
+ // left: padding + indicatorWidth + 6,
153
+ // top: (pillHeight - indicatorWidth) / 2,
154
+ // style: {
155
+ // text: `${pill.item.name}${chartType === 'line' || chartType === 'bar' || chartType === 'scatter' ? '' : ':'}`,
156
+ // fontSize: 14,
157
+ // fontWeight: 400,
158
+ // fill: isSelected && !isDisabled && total !== 0 ? '#1C1C1E' : '#81858A',
159
+ // textVerticalAlign: 'middle',
160
+ // },
161
+ // },
162
+ // ...(chartType === 'line' || chartType === 'bar' || chartType === 'scatter'
163
+ // ? []
164
+ // : [
165
+ // {
166
+ // type: 'text',
167
+ // right: padding,
168
+ // top: (pillHeight - indicatorWidth) / 2,
169
+ // style: {
170
+ // text:
171
+ // total === 0
172
+ // ? '-'
173
+ // : config?.valueFormatter
174
+ // ? config.valueFormatter(pill.item.value)
175
+ // : pill.item.value,
176
+ // fontSize: 14,
177
+ // fontWeight: 500,
178
+ // fill: isSelected && !isDisabled ? '#1C1C1E' : '#81858A',
179
+ // textAlign: 'right',
180
+ // textVerticalAlign: 'middle',
181
+ // },
182
+ // },
183
+ // ]),
184
+ // ],
185
+ // });
186
+ // xOffset += pill.pillWidth + spacing;
187
+ // });
188
+ // yOffset += pillHeight + spacing;
189
+ // });
190
+ // // 🔹 Position logic
191
+ // const positionConfig: any = {};
192
+ // switch (config?.legendPosition) {
193
+ // case 'top':
194
+ // positionConfig.top = config?.legendEdgeGap?.top ?? 20;
195
+ // break;
196
+ // case 'bottom':
197
+ // positionConfig.bottom = config?.legendEdgeGap?.bottom ?? 20;
198
+ // break;
199
+ // case 'left':
200
+ // positionConfig.left = config?.legendEdgeGap?.left ?? 20;
201
+ // break;
202
+ // case 'right':
203
+ // positionConfig.right = config?.legendEdgeGap?.right ?? 20;
204
+ // break;
205
+ // }
206
+ // // ALIGNMENT (Cross-Axis)
207
+ // if (config?.legendPosition === 'top' || config?.legendPosition === 'bottom') {
208
+ // // Horizontal area → align horizontally
209
+ // if (config?.legendAlign === 'center') {
210
+ // positionConfig.left = 'center';
211
+ // } else if (config?.legendAlign === 'right') {
212
+ // positionConfig.right = config?.legendEdgeGap?.right ?? 20;
213
+ // } else {
214
+ // positionConfig.left = config?.legendEdgeGap?.left ?? 20;
215
+ // }
216
+ // } else {
217
+ // // Vertical area → align vertically
218
+ // if (config?.legendAlign === 'center') {
219
+ // positionConfig.top = 'middle';
220
+ // } else if (config?.legendAlign === 'right') {
221
+ // positionConfig.bottom = config?.legendEdgeGap?.bottom ?? 20;
222
+ // } else {
223
+ // positionConfig.top = config?.legendEdgeGap?.top ?? 20;
224
+ // }
225
+ // }
226
+ // return [
227
+ // {
228
+ // id: 'customLegend',
229
+ // type: 'group',
230
+ // ...positionConfig,
231
+ // width: maxWidth,
232
+ // height: yOffset,
233
+ // children,
234
+ // },
235
+ // ];
236
+ // }
34
237
  buildGraphicLegend(total, config, chartInstance, chartType) {
35
238
  const pillHeight = 28;
36
239
  const spacing = 14;
37
240
  const indicatorWidth = 12;
38
241
  const padding = 8;
39
242
  const font = '14px sans-serif';
243
+ // =========================================
244
+ // CONTAINER + AVAILABLE WIDTH
245
+ // =========================================
40
246
  const containerWidth = chartInstance?.getWidth?.() ?? 800;
41
- const maxWidth = containerWidth * (config?.legendContainerWidth ?? 0.7);
247
+ const leftGap = config?.legendEdgeGap?.left ?? 0;
248
+ const rightGap = config?.legendEdgeGap?.right ?? 0;
249
+ // actual usable width after gaps
250
+ const usableWidth = containerWidth - leftGap - rightGap;
251
+ const maxWidth = usableWidth * (config?.legendContainerWidth ?? 0.7);
252
+ // =========================================
253
+ // SELECTED LEGEND MAP
254
+ // =========================================
42
255
  const selectedMap = chartInstance?.getOption()?.legend?.[0]?.selected || {};
43
256
  let colorIndex = 0;
257
+ // =========================================
258
+ // BUILD PILLS
259
+ // =========================================
44
260
  const pills = (chartType === 'line' || chartType === 'bar' || chartType === 'scatter'
45
261
  ? config?.series
46
262
  : config?.data)?.map((item) => {
@@ -67,30 +283,58 @@ class Charts {
67
283
  ? 0
68
284
  : 8 + valueRect.width) +
69
285
  padding;
70
- return { item, color, pillWidth };
286
+ return {
287
+ item,
288
+ color,
289
+ pillWidth,
290
+ };
71
291
  });
72
- // 🔹 Build rows dynamically
292
+ // =========================================
293
+ // BUILD ROWS PERFECTLY
294
+ // =========================================
73
295
  const rows = [];
74
296
  let currentRow = [];
75
297
  let rowWidth = 0;
76
298
  pills?.forEach((pill) => {
77
- if (rowWidth + pill.pillWidth > maxWidth && currentRow.length) {
78
- rows.push({ pills: currentRow, width: rowWidth });
79
- currentRow = [];
80
- rowWidth = 0;
299
+ const additionalWidth = currentRow.length > 0 ? spacing : 0;
300
+ // break row if width exceeded
301
+ if (currentRow.length && rowWidth + additionalWidth + pill.pillWidth > maxWidth) {
302
+ rows.push({
303
+ pills: currentRow,
304
+ width: rowWidth,
305
+ });
306
+ currentRow = [pill];
307
+ rowWidth = pill.pillWidth;
308
+ }
309
+ else {
310
+ rowWidth += additionalWidth + pill.pillWidth;
311
+ currentRow.push(pill);
81
312
  }
82
- currentRow.push(pill);
83
- rowWidth += pill.pillWidth + spacing;
84
313
  });
85
- if (currentRow.length)
86
- rows.push({ pills: currentRow, width: rowWidth });
87
- // 🔹 Build graphic children
314
+ if (currentRow.length) {
315
+ rows.push({
316
+ pills: currentRow,
317
+ width: rowWidth,
318
+ });
319
+ }
320
+ // =========================================
321
+ // BUILD GRAPHIC CHILDREN
322
+ // =========================================
88
323
  let yOffset = 0;
89
324
  const children = [];
90
325
  rows?.forEach((row) => {
91
- let xOffset = config?.legendAlign === 'center' && config?.legendDirection !== 'vertical'
92
- ? (maxWidth - row.width) / 2
93
- : maxWidth;
326
+ let xOffset = 0;
327
+ // =====================================
328
+ // HORIZONTAL ALIGNMENTS
329
+ // =====================================
330
+ if (config?.legendDirection !== 'vertical') {
331
+ if (config?.legendAlign === 'center') {
332
+ xOffset = Math.max((maxWidth - row.width) / 2, 0);
333
+ }
334
+ else if (config?.legendAlign === 'right') {
335
+ xOffset = Math.max(maxWidth - row.width, 0);
336
+ }
337
+ }
94
338
  row?.pills?.forEach((pill) => {
95
339
  const isSelected = selectedMap[pill.item.name] !== false;
96
340
  const isDisabled = pill.item.value === 0;
@@ -113,6 +357,9 @@ class Charts {
113
357
  });
114
358
  },
115
359
  children: [
360
+ // =================================
361
+ // BACKGROUND
362
+ // =================================
116
363
  {
117
364
  type: 'rect',
118
365
  shape: {
@@ -128,15 +375,25 @@ class Charts {
128
375
  lineWidth: 1,
129
376
  },
130
377
  },
378
+ // =================================
379
+ // INDICATOR
380
+ // =================================
131
381
  {
132
382
  type: 'rect',
133
- shape: { width: 12, height: config?.legendIndicatorHeight ?? 12, r: 2 },
383
+ shape: {
384
+ width: 12,
385
+ height: config?.legendIndicatorHeight ?? 12,
386
+ r: 2,
387
+ },
134
388
  left: padding,
135
389
  top: (pillHeight - (config?.legendIndicatorHeight ?? 12)) / 2,
136
390
  style: {
137
391
  fill: isSelected && !isDisabled ? pill.color : '#C7C7CC',
138
392
  },
139
393
  },
394
+ // =================================
395
+ // LABEL
396
+ // =================================
140
397
  {
141
398
  type: 'text',
142
399
  left: padding + indicatorWidth + 6,
@@ -149,6 +406,9 @@ class Charts {
149
406
  textVerticalAlign: 'middle',
150
407
  },
151
408
  },
409
+ // =================================
410
+ // VALUE
411
+ // =================================
152
412
  ...(chartType === 'line' || chartType === 'bar' || chartType === 'scatter'
153
413
  ? []
154
414
  : [
@@ -176,47 +436,54 @@ class Charts {
176
436
  });
177
437
  yOffset += pillHeight + spacing;
178
438
  });
179
- // 🔹 Position logic
439
+ // =========================================
440
+ // POSITION CONFIG
441
+ // =========================================
180
442
  const positionConfig = {};
181
443
  switch (config?.legendPosition) {
182
444
  case 'top':
183
- positionConfig.top = 20;
445
+ positionConfig.top = config?.legendEdgeGap?.top ?? 20;
184
446
  break;
185
447
  case 'bottom':
186
- positionConfig.bottom = 20;
448
+ positionConfig.bottom = config?.legendEdgeGap?.bottom ?? 20;
187
449
  break;
188
450
  case 'left':
189
- positionConfig.left = 20;
451
+ positionConfig.left = config?.legendEdgeGap?.left ?? 20;
190
452
  break;
191
453
  case 'right':
192
- positionConfig.right = 20;
454
+ positionConfig.right = config?.legendEdgeGap?.right ?? 20;
193
455
  break;
194
456
  }
195
- // ALIGNMENT (Cross-Axis)
457
+ // =========================================
458
+ // CROSS AXIS ALIGNMENT
459
+ // =========================================
196
460
  if (config?.legendPosition === 'top' || config?.legendPosition === 'bottom') {
197
- // Horizontal area → align horizontally
461
+ // horizontal placement
198
462
  if (config?.legendAlign === 'center') {
199
463
  positionConfig.left = 'center';
200
464
  }
201
465
  else if (config?.legendAlign === 'right') {
202
- positionConfig.right = 20;
466
+ positionConfig.right = config?.legendEdgeGap?.right ?? 20;
203
467
  }
204
468
  else {
205
- positionConfig.left = 20;
469
+ positionConfig.left = config?.legendEdgeGap?.left ?? 20;
206
470
  }
207
471
  }
208
472
  else {
209
- // Vertical area → align vertically
473
+ // vertical placement
210
474
  if (config?.legendAlign === 'center') {
211
475
  positionConfig.top = 'middle';
212
476
  }
213
477
  else if (config?.legendAlign === 'right') {
214
- positionConfig.bottom = 20;
478
+ positionConfig.bottom = config?.legendEdgeGap?.bottom ?? 20;
215
479
  }
216
480
  else {
217
- positionConfig.top = 20;
481
+ positionConfig.top = config?.legendEdgeGap?.top ?? 20;
218
482
  }
219
483
  }
484
+ // =========================================
485
+ // RETURN GRAPHIC
486
+ // =========================================
220
487
  return [
221
488
  {
222
489
  id: 'customLegend',
@@ -288,6 +555,36 @@ class Charts {
288
555
  const b = parseInt(hex.slice(5, 7), 16);
289
556
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
290
557
  }
558
+ formatPercentage(value) {
559
+ if (value === null || value === undefined || value === '')
560
+ return 'NA';
561
+ // already percentage string
562
+ if (typeof value === 'string' && value.includes('%'))
563
+ return value;
564
+ // numeric/string number
565
+ const num = Number(value);
566
+ if (isNaN(num))
567
+ return 'NA';
568
+ return `${num}%`;
569
+ }
570
+ getNumericValue(value) {
571
+ if (value === null || value === undefined || value === '')
572
+ return null;
573
+ // remove %
574
+ const cleaned = typeof value === 'string' ? value.replace('%', '').trim() : value;
575
+ const num = Number(cleaned);
576
+ return isNaN(num) ? null : num;
577
+ }
578
+ getStatusColor(value) {
579
+ const num = this.getNumericValue(value);
580
+ if (num === null)
581
+ return '#C0C2C5'; // grey
582
+ if (num < 30)
583
+ return '#EF4343'; // red
584
+ if (num <= 60)
585
+ return '#FFC107'; // yellow
586
+ return '#29A277'; // green
587
+ }
291
588
  pieTooltipFormatter(params, config) {
292
589
  const color = params.color;
293
590
  const name = params.name;
@@ -471,13 +768,13 @@ class Charts {
471
768
 
472
769
  <div style="margin-top:6px;">
473
770
  Productivity:
474
- <span style="color:#f59e0b;font-weight:600;">
475
- ${d.productivity || 'NA'}
771
+ <span style="color:${this.getStatusColor(d.productivity)};font-weight:600;">
772
+ ${this.formatPercentage(d.productivity)}
476
773
  </span>
477
774
 
478
775
  <span style="margin-left:12px;">Activity:</span>
479
- <span style="color:#22c55e;font-weight:600;">
480
- ${d.activity || 'NA'}
776
+ <span style="color:${this.getStatusColor(d.activity)};font-weight:600;">
777
+ ${this.formatPercentage(d.activity)}
481
778
  </span>
482
779
  </div>
483
780
 
@@ -657,6 +954,51 @@ class LinesChart {
657
954
  }
658
955
  this.buildOption();
659
956
  }
957
+ previousConfig = {
958
+ xAxisName: '',
959
+ yAxisName: '',
960
+ secondaryYAxisName: '',
961
+ title: '',
962
+ };
963
+ ngDoCheck() {
964
+ if (!this.chartInstance || !this.config)
965
+ return;
966
+ const updateOption = {};
967
+ if (this.previousConfig.xAxisName !== this.config.xAxisName) {
968
+ this.previousConfig.xAxisName = this.config.xAxisName;
969
+ updateOption.xAxis = {
970
+ name: this.config?.xAxisName || '',
971
+ };
972
+ }
973
+ if (this.previousConfig.yAxisName !== this.config.yAxisName) {
974
+ this.previousConfig.yAxisName = this.config.yAxisName;
975
+ updateOption.yAxis = [
976
+ {
977
+ name: this.config?.yAxisName || '',
978
+ },
979
+ ];
980
+ }
981
+ if (this.previousConfig.secondaryYAxisName !== this.config.secondaryYAxisName) {
982
+ this.previousConfig.secondaryYAxisName = this.config.secondaryYAxisName;
983
+ updateOption.yAxis = [
984
+ {
985
+ name: this.config?.yAxisName || '',
986
+ },
987
+ {
988
+ name: this.config?.secondaryYAxisName || '',
989
+ },
990
+ ];
991
+ }
992
+ if (this.previousConfig.title !== this.config.title) {
993
+ this.previousConfig.title = this.config.title;
994
+ updateOption.title = {
995
+ text: this.config?.title || '',
996
+ };
997
+ }
998
+ if (Object.keys(updateOption).length) {
999
+ this.chartInstance.setOption(updateOption);
1000
+ }
1001
+ }
660
1002
  /* ------------------ OPTION BUILDER ------------------ */
661
1003
  buildOption() {
662
1004
  this.chartOption = {
@@ -2046,6 +2388,7 @@ class CustomChartComponent {
2046
2388
  data: [
2047
2389
  {
2048
2390
  ...item,
2391
+ category: track.category,
2049
2392
  value: [
2050
2393
  categories.indexOf(track.category),
2051
2394
  this.convertTimeToNumber(item.start),
@@ -2071,7 +2414,12 @@ class CustomChartComponent {
2071
2414
  borderWidth: 0,
2072
2415
  padding: 0,
2073
2416
  extraCssText: 'box-shadow:none;',
2074
- formatter: (params) => this.chartService.timelineChartTooltipFormatter(params),
2417
+ formatter: (params) => {
2418
+ const tooltipCategories = this.config.tooltipCategories;
2419
+ if (tooltipCategories?.length && !tooltipCategories.includes(params.data?.category))
2420
+ return '';
2421
+ return this.chartService.timelineChartTooltipFormatter(params);
2422
+ },
2075
2423
  },
2076
2424
  legend: this.buildLegend(),
2077
2425
  grid: {