@nyaruka/temba-components 0.124.2 → 0.124.3

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.
@@ -3,33 +3,223 @@ import { property, state } from 'lit/decorators.js';
3
3
  import { css, html, PropertyValueMap, TemplateResult } from 'lit';
4
4
 
5
5
  import { Select, SelectOption } from '../select/Select';
6
- import { getClasses } from '../utils';
6
+ import { darkenColor, getClasses } from '../utils';
7
7
  import { getStore } from '../store/Store';
8
8
 
9
9
  // eslint-disable-next-line import/no-named-as-default
10
10
  import Chart, { ChartType } from 'chart.js/auto';
11
11
  import 'chartjs-adapter-luxon';
12
12
 
13
- const colors = [
14
- 'rgba(54, 162, 235, 0.2)',
15
- 'rgba(255, 159, 64, 0.2)',
16
- 'rgba(75, 192, 192, 0.2)',
17
- 'rgba(153, 102, 255, 0.2)',
18
- 'rgba(255, 205, 86, 0.2)',
19
- 'rgba(255, 99, 132, 0.2)'
20
- ];
21
-
22
- const colorsBorder = [
23
- 'rgb(54, 162, 235)',
24
- 'rgb(255, 159, 64)',
25
- 'rgb(75, 192, 192)',
26
- 'rgb(153, 102, 255)',
27
- 'rgb(255, 205, 86)',
28
- 'rgb(255, 99, 132)'
29
- ];
30
-
31
- const otherBackgroundColor = 'rgba(201, 203, 207, 0.2)';
32
- const otherBorderColor = 'rgb(201, 203, 207)';
13
+ const DEFAULT_PALETTE: keyof typeof COLOR_PALETTES = 'qualitative-set1';
14
+ const COLOR_PALETTES = {
15
+ // Qualitative (categorical, no order)
16
+ 'qualitative-set1': [
17
+ '#5ea3db',
18
+ '#c186e3',
19
+ '#66c2a5',
20
+ '#fc8d62',
21
+ '#a6d854',
22
+ '#ffd92f',
23
+ '#e5c494',
24
+ '#b3b3b3'
25
+ ],
26
+ 'qualitative-set2': [
27
+ '#377eb8',
28
+ '#984ea3',
29
+ '#4daf4a',
30
+ '#ff7f00',
31
+ '#e41a1c',
32
+ '#a65628',
33
+ '#f781bf',
34
+ '#ffff33'
35
+ ],
36
+ 'qualitative-set3': [
37
+ '#1b9e77',
38
+ '#d95f02',
39
+ '#7570b3',
40
+ '#e7298a',
41
+ '#66a61e',
42
+ '#e6ab02',
43
+ '#a6761d'
44
+ ],
45
+ 'qualitative-paired': [
46
+ '#1f78b4',
47
+ '#a6cee3',
48
+ '#6a3d9a',
49
+ '#cab2d6',
50
+ '#33a02c',
51
+ '#b2df8a',
52
+ '#e31a1c',
53
+ '#fb9a99',
54
+ '#ff7f00',
55
+ '#fdbf6f'
56
+ ],
57
+ 'qualitative-accent': [
58
+ '#7fc97f',
59
+ '#beaed4',
60
+ '#fdc086',
61
+ '#ffff99',
62
+ '#386cb0',
63
+ '#f0027f',
64
+ '#bf5b17',
65
+ '#666666'
66
+ ],
67
+ 'qualitative-pastel1': [
68
+ '#fbb4ae',
69
+ '#b3cde3',
70
+ '#ccebc5',
71
+ '#decbe4',
72
+ '#fed9a6',
73
+ '#ffffcc',
74
+ '#e5d8bd',
75
+ '#fddaec'
76
+ ],
77
+ 'qualitative-pastel2': [
78
+ '#b3e2cd',
79
+ '#fdcdac',
80
+ '#cbd5e8',
81
+ '#f4cae4',
82
+ '#e6f5c9',
83
+ '#fff2ae',
84
+ '#f1e2cc'
85
+ ],
86
+ // Diverging (for data with midpoint like 0)
87
+ 'diverging-prgn': [
88
+ '#40004b',
89
+ '#762a83',
90
+ '#9970ab',
91
+ '#c2a5cf',
92
+ '#e7d4e8',
93
+ '#f7f7f7',
94
+ '#d9f0d3',
95
+ '#a6dba0',
96
+ '#5aae61',
97
+ '#1b7837',
98
+ '#00441b'
99
+ ],
100
+ 'diverging-spectral': [
101
+ '#9e0142',
102
+ '#d53e4f',
103
+ '#f46d43',
104
+ '#fdae61',
105
+ '#fee08b',
106
+ '#ffffbf',
107
+ '#e6f598',
108
+ '#abdda4',
109
+ '#66c2a5',
110
+ '#3288bd',
111
+ '#5e4fa2'
112
+ ],
113
+ 'diverging-piyg': [
114
+ '#8e0152',
115
+ '#c51b7d',
116
+ '#de77ae',
117
+ '#f1b6da',
118
+ '#fde0ef',
119
+ '#f7f7f7',
120
+ '#e6f5d0',
121
+ '#b8e186',
122
+ '#7fbc41',
123
+ '#4d9221',
124
+ '#276419'
125
+ ],
126
+ 'diverging-rdylgn': [
127
+ '#a50026',
128
+ '#d73027',
129
+ '#f46d43',
130
+ '#fdae61',
131
+ '#fee08b',
132
+ '#ffffbf',
133
+ '#d9ef8b',
134
+ '#a6d96a',
135
+ '#66bd63',
136
+ '#1a9850',
137
+ '#006837'
138
+ ],
139
+ 'diverging-brbg': [
140
+ '#543005',
141
+ '#8c510a',
142
+ '#bf812d',
143
+ '#dfc27d',
144
+ '#f6e8c3',
145
+ '#f5f5f5',
146
+ '#c7eae5',
147
+ '#80cdc1',
148
+ '#35978f',
149
+ '#01665e',
150
+ '#003c30'
151
+ ],
152
+
153
+ // Sequential (for continuous or ordered data)
154
+ 'sequential-blues': [
155
+ '#f7fbff',
156
+ '#deebf7',
157
+ '#c6dbef',
158
+ '#9ecae1',
159
+ '#6baed6',
160
+ '#4292c6',
161
+ '#2171b5',
162
+ '#08519c',
163
+ '#08306b'
164
+ ],
165
+ 'sequential-greens': [
166
+ '#f7fcf5',
167
+ '#e5f5e0',
168
+ '#c7e9c0',
169
+ '#a1d99b',
170
+ '#74c476',
171
+ '#41ab5d',
172
+ '#238b45',
173
+ '#006d2c',
174
+ '#00441b'
175
+ ],
176
+ 'sequential-oranges': [
177
+ '#fff5eb',
178
+ '#fee6ce',
179
+ '#fdd0a2',
180
+ '#fdae6b',
181
+ '#fd8d3c',
182
+ '#f16913',
183
+ '#d94801',
184
+ '#a63603',
185
+ '#7f2704'
186
+ ],
187
+ 'sequential-purples': [
188
+ '#fcfbfd',
189
+ '#efedf5',
190
+ '#dadaeb',
191
+ '#bcbddc',
192
+ '#9e9ac8',
193
+ '#807dba',
194
+ '#6a51a3',
195
+ '#54278f',
196
+ '#3f007d'
197
+ ],
198
+ 'sequential-reds': [
199
+ '#fff5f0',
200
+ '#fee0d2',
201
+ '#fcbba1',
202
+ '#fc9272',
203
+ '#fb6a4a',
204
+ '#ef3b2c',
205
+ '#cb181d',
206
+ '#a50f15',
207
+ '#67000d'
208
+ ],
209
+ 'sequential-ylgnbu': [
210
+ '#ffffd9',
211
+ '#edf8b1',
212
+ '#c7e9b4',
213
+ '#7fcdbb',
214
+ '#41b6c4',
215
+ '#1d91c0',
216
+ '#225ea8',
217
+ '#253494',
218
+ '#081d58'
219
+ ]
220
+ };
221
+
222
+ const otherBackgroundColor = 'rgba(212, 212, 212, 0.5)';
33
223
 
34
224
  /**
35
225
  * Formats a duration in seconds to a human-readable string showing the two largest units.
@@ -126,6 +316,18 @@ export class TembaChart extends RapidElement {
126
316
  @state()
127
317
  showConfig: boolean = false;
128
318
 
319
+ @property({ type: String })
320
+ palette: keyof typeof COLOR_PALETTES;
321
+
322
+ @property({ type: Number })
323
+ opacity: number = 1;
324
+
325
+ @property({ type: Number })
326
+ seriesBorderRadius: number = 2;
327
+
328
+ @property({ type: Number })
329
+ seriesBorderWidth: number = 1;
330
+
129
331
  chart: Chart;
130
332
  shadowRootDiv: HTMLDivElement;
131
333
  canvas: HTMLCanvasElement;
@@ -214,19 +416,82 @@ export class TembaChart extends RapidElement {
214
416
  this.data = response.json.data;
215
417
  });
216
418
  }
419
+
420
+ if (changes.has('chartType')) {
421
+ if (this.chartType === 'line') {
422
+ this.seriesBorderWidth = Math.max(1, this.seriesBorderWidth);
423
+ }
424
+ }
217
425
  }
218
426
 
427
+ /**
428
+ * Returns a tuple: [backgroundColors[], borderColors[]].
429
+ * Border colors are darkened versions of the base palette (before transparency).
430
+ * Background colors have transparency applied.
431
+ */
432
+ get colors(): [string[], string[]] {
433
+ const baseColors =
434
+ COLOR_PALETTES[this.palette] || COLOR_PALETTES[DEFAULT_PALETTE];
435
+ // Clamp transparency between 0 and 1
436
+ const alpha = Math.max(0, Math.min(1, this.opacity));
437
+ // Borders: darken base color, no transparency
438
+ const borderColors = baseColors.map((color) => darkenColor(color, 0.25));
439
+ // Backgrounds: apply transparency to base color
440
+ const backgroundColors = baseColors.map((color) => {
441
+ // If already rgba, just replace the alpha
442
+ if (color.startsWith('rgba')) {
443
+ return color.replace(
444
+ /rgba\(([^,]+),([^,]+),([^,]+),([^)]+)\)/,
445
+ (_m, r, g, b) => {
446
+ return `rgba(${r},${g},${b},${alpha})`;
447
+ }
448
+ );
449
+ }
450
+ // If already rgb, convert to rgba
451
+ if (color.startsWith('rgb(')) {
452
+ return color.replace(
453
+ /rgb\(([^,]+),([^,]+),([^,]+)\)/,
454
+ (_m, r, g, b) => {
455
+ return `rgba(${r},${g},${b},${alpha})`;
456
+ }
457
+ );
458
+ }
459
+ // If hex, convert to rgba
460
+ if (color.startsWith('#')) {
461
+ let hex = color.replace('#', '');
462
+ if (hex.length === 3) {
463
+ hex = hex
464
+ .split('')
465
+ .map((c) => c + c)
466
+ .join('');
467
+ }
468
+ const num = parseInt(hex, 16);
469
+ const r = (num >> 16) & 255;
470
+ const g = (num >> 8) & 255;
471
+ const b = num & 255;
472
+ return `rgba(${r},${g},${b},${alpha})`;
473
+ }
474
+ // fallback
475
+ return color;
476
+ });
477
+ return [backgroundColors, borderColors];
478
+ }
479
+
480
+ /**
481
+ * Utility to darken an rgba or hex color by a given factor (0-1).
482
+ */
483
+
219
484
  private calculateSplits() {
220
485
  if (this.data) {
221
486
  const datasets = [];
222
- // keep a running list of values that is the sum at each index
223
487
  const sums = [];
488
+ // Get color arrays
489
+ const [backgroundColors, borderColors] = this.colors;
224
490
  for (const dataset of this.data.datasets) {
225
491
  if (
226
492
  !this.showAll &&
227
493
  this.splits.find((s) => s === dataset.label) === undefined
228
494
  ) {
229
- // update our sums
230
495
  for (let i = 0; i < dataset.data.length; i++) {
231
496
  if (sums[i] === undefined) {
232
497
  sums[i] = dataset.data[i];
@@ -235,26 +500,29 @@ export class TembaChart extends RapidElement {
235
500
  }
236
501
  }
237
502
  } else {
503
+ const colorIdx =
504
+ (datasets.length + this.colorIndex) % backgroundColors.length;
505
+ const bgColor = backgroundColors[colorIdx];
506
+ const borderColor = borderColors[colorIdx];
238
507
  datasets.push({
239
508
  ...dataset,
240
- backgroundColor:
241
- colors[(datasets.length + this.colorIndex) % colors.length],
242
- borderColor:
243
- colorsBorder[
244
- (datasets.length + this.colorIndex) % colorsBorder.length
245
- ],
246
- borderWidth: 1
509
+ backgroundColor: bgColor,
510
+ borderColor,
511
+ borderWidth: this.seriesBorderWidth,
512
+ borderRadius: this.seriesBorderRadius
247
513
  });
248
514
  }
249
515
  }
250
516
 
251
517
  if (datasets.length === 0) {
518
+ const idx = this.colorIndex % backgroundColors.length;
252
519
  datasets.push({
253
520
  label: this.single ? this.dataname : `All ${this.dataname}`,
254
521
  data: sums,
255
- backgroundColor: colors[this.colorIndex % colors.length],
256
- borderColor: colorsBorder[this.colorIndex % colorsBorder.length],
257
- borderWidth: 1
522
+ backgroundColor: backgroundColors[idx],
523
+ borderColor: borderColors[idx],
524
+ borderWidth: this.seriesBorderWidth,
525
+ borderRadius: this.seriesBorderRadius
258
526
  });
259
527
  } else {
260
528
  if (!this.hideOther && !this.showAll) {
@@ -262,8 +530,9 @@ export class TembaChart extends RapidElement {
262
530
  label: 'Other',
263
531
  data: sums,
264
532
  backgroundColor: otherBackgroundColor,
265
- borderColor: otherBorderColor,
266
- borderWidth: 1
533
+ borderColor: darkenColor(otherBackgroundColor, 0.05),
534
+ borderWidth: 1,
535
+ borderRadius: this.seriesBorderRadius
267
536
  });
268
537
  }
269
538
  }
@@ -318,7 +587,8 @@ export class TembaChart extends RapidElement {
318
587
  return formatDurationFromSeconds(value);
319
588
  }
320
589
  }
321
- })
590
+ }),
591
+ grid: { color: 'rgba(0,0,0,0.04)' }
322
592
  },
323
593
  x: {
324
594
  type: 'time',
@@ -561,6 +561,7 @@ export class Store extends RapidElement {
561
561
  const orginalUser = last[parts[parts.length - 1]];
562
562
  orginalUser.avatar = user.avatar;
563
563
  orginalUser.name = getFullName(user);
564
+ orginalUser.uuid = user.uuid;
564
565
  last[parts[parts.length - 1]].avatar = user.avatar;
565
566
  }
566
567
  });
@@ -802,6 +802,46 @@ export const hslToHex = (h, s, l) => {
802
802
  return `#${f(0)}${f(8)}${f(4)}`;
803
803
  };
804
804
 
805
+ export const darkenColor = (color: string, factor: number): string => {
806
+ // If rgba or rgb
807
+ const rgbaMatch = color.match(
808
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/
809
+ );
810
+ if (rgbaMatch) {
811
+ // eslint-disable-next-line prefer-const
812
+ let [r, g, b, a] = rgbaMatch
813
+ .slice(1)
814
+ .map((v, i) => (i < 3 ? parseInt(v) : parseFloat(v)));
815
+ r = Math.max(0, Math.floor(r * (1 - factor)));
816
+ g = Math.max(0, Math.floor(g * (1 - factor)));
817
+ b = Math.max(0, Math.floor(b * (1 - factor)));
818
+ if (rgbaMatch[4] !== undefined) {
819
+ return `rgba(${r},${g},${b},${a})`;
820
+ }
821
+ return `rgb(${r},${g},${b})`;
822
+ }
823
+ // If hex
824
+ if (color.startsWith('#')) {
825
+ let hex = color.replace('#', '');
826
+ if (hex.length === 3) {
827
+ hex = hex
828
+ .split('')
829
+ .map((c) => c + c)
830
+ .join('');
831
+ }
832
+ const num = parseInt(hex, 16);
833
+ let r = (num >> 16) & 255;
834
+ let g = (num >> 8) & 255;
835
+ let b = num & 255;
836
+ r = Math.max(0, Math.floor(r * (1 - factor)));
837
+ g = Math.max(0, Math.floor(g * (1 - factor)));
838
+ b = Math.max(0, Math.floor(b * (1 - factor)));
839
+ return `rgb(${r},${g},${b})`;
840
+ }
841
+ // fallback
842
+ return color;
843
+ };
844
+
805
845
  export const renderAvatar = (input: {
806
846
  name?: string;
807
847
  user?: User;