Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import Constants from "../../core/Constants";
import type { MSTControlTag } from "../../stores/types";
import { contrastColor, convertToRGBA } from "../../utils/colors";
import type { ControlTag, LabelColors, LabelCounts } from "./types";
 
const defaultLabelColor = Constants.LABEL_BACKGROUND;
 
/**
 * Generate light pastel-like background colors for label chips, cycling through
 * a palette of visually distinct color hues. Shades are intentionally light
 * and remain above the RGB threshold of 200 to ensure chip backgrounds are
 * always easily readable against white or light UIs.
 *
 * The method uses a fixed palette of "anchor" hues, moving to a different one
 * per index, and after each cycle, will decrease their intensity slightly (but
 * never below 200 per channel) to generate additional distinct but consistent shades.
 *
 * @param {number} index - The index in the palette (for color assignment)
 * @returns {string} - RGBA string of the computed background color
 */
const generateBackgroundColor = (index: number): string => {
  // Palette of anchor pastel colors (each as [r,g,b])
  const baseColors: [number, number, number][] = [
    // [250, 255, 250], // Light green
    [255, 255, 230], // Light yellow
    [247, 237, 250], // Light purple
    [240, 248, 255], // Alice blue (very light blue)
    [255, 240, 250], // Lavender blush (soft pink)
    [250, 250, 210], // Light goldenrod yellow
    [230, 255, 250], // Light cyan/turquoise
    [242, 255, 242], // Very pale mint
    [255, 250, 220], // Beige/cream
    [255, 245, 238], // Sea shell (pale coral)
    [225, 240, 255], // Pale blue-gray
    [246, 246, 232], // Soft creamy
  ];
 
  // How many base colors in our palette?
  const paletteLength = baseColors.length;
 
  // How many cycles have we gone through? (Each "cycle" darkens the shade)
  const cycle = Math.floor(index / paletteLength);
 
  // Index in our fixed palette
  const colorIdx = index % paletteLength;
 
  // Amount to decrease each channel per cycle (how quickly to darken)
  const decrementPerCycle = 10;
 
  // Get anchor color and adjust its channels
  let [r, g, b] = baseColors[colorIdx];
 
  // Reduce channels by cycle amount, but don't let any fall below 200 (to stay bright)
  const minChannelValue = 200;
  r = Math.max(r - decrementPerCycle * cycle, minChannelValue);
  g = Math.max(g - decrementPerCycle * cycle, minChannelValue);
  b = Math.max(b - decrementPerCycle * cycle, minChannelValue);
 
  // Use a soft, readable alpha
  const alpha = 0.8;
 
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
 
/**
 * Get label colors for a control. Calculates background and border colors for each label.
 * @param control
 * @returns record of label colors
 */
export const getLabelColors = (control: MSTControlTag) => {
  if (!control.children) return {};
 
  const labelColors: Record<string, LabelColors> = {};
  let index = 0;
 
  for (const item of control.children) {
    const color = item.background;
    const background = color ? convertToRGBA(color, 0.3) : undefined;
 
    labelColors[item.value] = {
      value: item.value,
      border: color ?? defaultLabelColor,
      color: background ? contrastColor(background) : undefined,
      background: background ?? generateBackgroundColor(index),
    };
    index++;
  }
 
  return labelColors;
};
 
/**
 * Get label counts for a control. Extends color info with count.
 * @param {string[]} labels - list of labels to count
 * @param labelColors - record of label colors from `getLabelColors()`
 * @returns record with label counts and colors
 */
export const getLabelCounts = (labels: string[], labelColors: Record<string, LabelColors>) => {
  const labelCounts: Record<string, LabelCounts> = Object.fromEntries(
    Object.entries(labelColors).map(([lbl, attr]) => [lbl, { ...attr, count: 0 }]),
  );
  const defaultData = {
    background: convertToRGBA(defaultLabelColor, 0.3),
    border: defaultLabelColor,
    count: 0,
  };
 
  for (const label of labels) {
    let data = labelCounts[label];
    if (!data) {
      data = labelCounts[label] = { ...defaultData, value: label };
    }
    data.count++;
  }
 
  return labelCounts;
};
 
/**
 * Sort controls: global classifications first, then labels, then per-regions
 * @param controls - list of controls to sort
 * @returns sorted list of controls
 */
export const sortControls = (controls: ControlTag[]) => {
  return controls.sort((a, b) => {
    if (a.per_region && !b.per_region) return 1;
    if (!a.per_region && b.per_region) return -1;
    // for non-per-region controls, put classification controls first and labels next
    if (a.type.endsWith("labels") && !b.type.endsWith("labels")) return 1;
    if (!a.type.endsWith("labels") && b.type.endsWith("labels")) return -1;
    return 0;
  });
};