@milaboratories/uikit 2.2.37 → 2.2.39

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.2.37",
3
+ "version": "2.2.39",
4
4
  "type": "module",
5
5
  "main": "dist/pl-uikit.umd.js",
6
6
  "module": "dist/pl-uikit.js",
@@ -32,9 +32,9 @@
32
32
  "vue-tsc": "^2.1.10",
33
33
  "yarpm": "^1.2.0",
34
34
  "svgo": "^3.3.2",
35
+ "@milaboratories/eslint-config": "^1.0.1",
35
36
  "@milaboratories/helpers": "^1.6.11",
36
- "@platforma-sdk/model": "^1.20.11",
37
- "@milaboratories/eslint-config": "^1.0.0"
37
+ "@platforma-sdk/model": "^1.20.27"
38
38
  },
39
39
  "scripts": {
40
40
  "dev": "vite",
@@ -0,0 +1,27 @@
1
+ import { describe } from 'node:test';
2
+ import { expect, test } from 'vitest';
3
+ import { Gradient } from '../gradient';
4
+ import { viridis } from '../palette';
5
+ import { Color } from '../color';
6
+
7
+ describe('Colors', () => {
8
+ test('gradients', () => {
9
+ const viridis5colors = Gradient(viridis).split(5);
10
+
11
+ viridis5colors.forEach((color, i) => {
12
+ expect(color.hex).toEqual(Gradient(viridis).getNthOf(i + 1, 5).hex);
13
+ });
14
+
15
+ const viridis15 = Gradient(viridis).split(15);
16
+
17
+ console.log('viridis15', JSON.stringify(viridis15));
18
+
19
+ expect(viridis.map((it) => it + 'FF').join(',')).toEqual(viridis15.map((it) => it.hex.toUpperCase()).join(','));
20
+ });
21
+
22
+ test('categorical colors', () => {
23
+ const color = Color.categorical('lime_light');
24
+
25
+ expect(color.hex.toUpperCase()).toEqual('#CBEB67FF');
26
+ });
27
+ });
@@ -0,0 +1,80 @@
1
+ import { categoricalColors, type CategoricalColor } from './palette';
2
+
3
+ /**
4
+ * Represents a color with red, green, blue, and alpha channels.
5
+ * Provides methods to convert to HEX and RGBA formats.
6
+ *
7
+ * @param {number} r - Red channel (0-255).
8
+ * @param {number} g - Green channel (0-255).
9
+ * @param {number} b - Blue channel (0-255).
10
+ * @param {number} [a=1] - Alpha channel (0-1).
11
+ */
12
+ export function Color(r: number, g: number, b: number, a: number = 1) {
13
+ return new class {
14
+ constructor(
15
+ public readonly r: number,
16
+ public readonly g: number,
17
+ public readonly b: number,
18
+ public readonly a: number = 1,
19
+ ) {}
20
+
21
+ get hex() {
22
+ const hexR = r.toString(16).padStart(2, '0');
23
+ const hexG = g.toString(16).padStart(2, '0');
24
+ const hexB = b.toString(16).padStart(2, '0');
25
+ const hexA = Math.round(a * 255).toString(16).padStart(2, '0'); // Alpha in 2-digit hex
26
+
27
+ return `#${hexR}${hexG}${hexB}${hexA}`;
28
+ }
29
+
30
+ get rgba() {
31
+ return `rgb(${r}, ${g}, ${b}, ${a})`;
32
+ }
33
+
34
+ toString() {
35
+ return this.hex;
36
+ }
37
+
38
+ toJSON() {
39
+ return this.hex;
40
+ }
41
+ }(r, g, b, a);
42
+ }
43
+
44
+ export type Color = ReturnType<typeof Color>;
45
+
46
+ Color.fromHex = (hex: string): Color => {
47
+ hex = hex.replace('#', '');
48
+
49
+ let r: number, g: number, b: number, a: number = 1;
50
+
51
+ if (hex.length === 6) {
52
+ r = parseInt(hex.slice(0, 2), 16);
53
+ g = parseInt(hex.slice(2, 4), 16);
54
+ b = parseInt(hex.slice(4, 6), 16);
55
+ } else if (hex.length === 8) {
56
+ r = parseInt(hex.slice(0, 2), 16);
57
+ g = parseInt(hex.slice(2, 4), 16);
58
+ b = parseInt(hex.slice(4, 6), 16);
59
+ a = parseInt(hex.slice(6, 8), 16) / 255;
60
+ } else {
61
+ throw new Error('Invalid HEX color format.');
62
+ }
63
+
64
+ return Color(r, g, b, a);
65
+ };
66
+
67
+ /**
68
+ * Parses a color string (attention: currently supports only HEX, @todo)
69
+ */
70
+ Color.fromString = (str: string) => {
71
+ if (str.startsWith('#')) {
72
+ return Color.fromHex(str);
73
+ }
74
+
75
+ throw Error('TODO: implement rgb(a), hsl');
76
+ };
77
+
78
+ Color.categorical = (name: CategoricalColor) => {
79
+ return Color.fromHex(categoricalColors[name]);
80
+ };
@@ -0,0 +1,136 @@
1
+ import type { Palette } from './palette';
2
+ import { palettes } from './palette';
3
+ import { Color } from './color';
4
+
5
+ export type GradientSource = (string | Color)[] | Palette;
6
+
7
+ Color.fromHex = (hex: string): Color => {
8
+ hex = hex.replace('#', '');
9
+
10
+ let r: number, g: number, b: number, a: number = 1;
11
+
12
+ if (hex.length === 6) {
13
+ r = parseInt(hex.slice(0, 2), 16);
14
+ g = parseInt(hex.slice(2, 4), 16);
15
+ b = parseInt(hex.slice(4, 6), 16);
16
+ } else if (hex.length === 8) {
17
+ r = parseInt(hex.slice(0, 2), 16);
18
+ g = parseInt(hex.slice(2, 4), 16);
19
+ b = parseInt(hex.slice(4, 6), 16);
20
+ a = parseInt(hex.slice(6, 8), 16) / 255;
21
+ } else {
22
+ throw new Error('Invalid HEX color format.');
23
+ }
24
+
25
+ return Color(r, g, b, a);
26
+ };
27
+
28
+ /**
29
+ * Parses a color string (attention: currently supports only HEX, @todo)
30
+ */
31
+ Color.fromString = (str: string) => {
32
+ if (str.startsWith('#')) {
33
+ return Color.fromHex(str);
34
+ }
35
+
36
+ throw Error('TODO: implement rgb(a), hsl');
37
+ };
38
+
39
+ function lerp(a: number, b: number, t: number): number {
40
+ return a + t * (b - a);
41
+ }
42
+
43
+ /**
44
+ * Interpolates between two colors.
45
+ *
46
+ * @param {Color} color1 - Start color.
47
+ * @param {Color} color2 - End color.
48
+ * @param {number} t - Interpolation factor [0, 1].
49
+ * @returns {Color} Interpolated color.
50
+ */
51
+ export function interpolateColor(color1: Color, color2: Color, t: number): Color {
52
+ const r = Math.round(lerp(color1.r, color2.r, t));
53
+ const g = Math.round(lerp(color1.g, color2.g, t));
54
+ const b = Math.round(lerp(color1.b, color2.b, t));
55
+ return Color(r, g, b);
56
+ }
57
+
58
+ /**
59
+ * Normalizes a gradient definition into an array of Color objects.
60
+ *
61
+ * @param {GradientSource} raw - A gradient defined as an array of strings, Colors, or a Palette.
62
+ * @returns {Color[]} Array of normalized Color objects.
63
+ */
64
+ export function normalizeGradient(raw: GradientSource): Color[] {
65
+ if (typeof raw === 'string') {
66
+ return palettes[raw].map((it) => Color.fromString(it));
67
+ }
68
+
69
+ return raw.map((it) => {
70
+ if (typeof it === 'string') {
71
+ return Color.fromString(it);
72
+ }
73
+
74
+ return it;
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Creates a gradient with utilities to sample or split colors.
80
+ */
81
+ export function Gradient(gradient: GradientSource) {
82
+ return new class {
83
+ constructor(public readonly colors: Color[]) {}
84
+
85
+ /**
86
+ * Samples a color at a specific point in the gradient.
87
+ *
88
+ * @param {number} t - A value in [0, 1] representing the position in the gradient.
89
+ */
90
+ fromInterval(t: number) {
91
+ if (t < 0) throw new Error('t must be greater than or equal to 0');
92
+ if (t > 1) throw new Error('t must be less than or equal to 1');
93
+
94
+ const colors = this.colors;
95
+
96
+ const segments = colors.length - 1;
97
+
98
+ const segment = Math.floor(t * segments);
99
+
100
+ const localT = (t * segments) % 1; // Local t within the current segment
101
+
102
+ const color1 = colors[segment];
103
+ const color2 = colors[Math.min(segment + 1, segments)];
104
+
105
+ return interpolateColor(color1, color2, localT);
106
+ }
107
+
108
+ /**
109
+ * Gets the nth color in a gradient divided into segments.
110
+ *
111
+ * @param {number} n - Index of the color (1-based).
112
+ * @param {number} segments - Total number of segments.
113
+ */
114
+ getNthOf(n: number, segments: number) {
115
+ if (n <= 0) throw new Error('n must be greater than 0');
116
+ if (n > segments) throw Error('n must be lower or equal than count of segments');
117
+ return this.fromInterval((n - 1) / (segments - 1));
118
+ }
119
+
120
+ /**
121
+ * Splits the gradient into n evenly spaced colors.
122
+ */
123
+ split(n: number) {
124
+ if (n <= 0) throw new Error('n must be greater than 0');
125
+
126
+ const colors: Color[] = [];
127
+
128
+ for (let i = 0; i < n; i++) {
129
+ const t = i / (n - 1); // Normalize t to [0, 1]
130
+ colors.push(this.fromInterval(t));
131
+ }
132
+
133
+ return colors;
134
+ }
135
+ }(normalizeGradient(gradient));
136
+ }
@@ -0,0 +1,3 @@
1
+ export * from './palette';
2
+ export * from './color';
3
+ export * from './gradient';
@@ -0,0 +1,283 @@
1
+ /**
2
+ * good for age range // from newborn → to old
3
+ */
4
+ export const viridis = [
5
+ '#FFF680',
6
+ '#E8F66C',
7
+ '#C4F16B',
8
+ '#9AEB71',
9
+ '#70E084',
10
+ '#43D18A',
11
+ '#2DBD96',
12
+ '#28A8A0',
13
+ '#2793A3',
14
+ '#337B9E',
15
+ '#3B6399',
16
+ '#424C8F',
17
+ '#4A3584',
18
+ '#481B70',
19
+ '#4A005C',
20
+ ];
21
+
22
+ /**
23
+ * from light → to hard (errors)
24
+ */
25
+ export const magma = [
26
+ '#FFF680',
27
+ '#FFE871',
28
+ '#FDCD6F',
29
+ '#FEAD66',
30
+ '#FA935F',
31
+ '#F57258',
32
+ '#EB555E',
33
+ '#D64470',
34
+ '#B83778',
35
+ '#982D82',
36
+ '#7E2584',
37
+ '#611B84',
38
+ '#49187A',
39
+ '#38116B',
40
+ '#2B125C',
41
+ ];
42
+
43
+ /**
44
+ * From light to hard
45
+ */
46
+ const density = [
47
+ '#DFFADC',
48
+ '#C9F5D3',
49
+ '#B3F2CF',
50
+ '#9AEBCD',
51
+ '#80DCCC',
52
+ '#6DC8D2',
53
+ '#61B7DB',
54
+ '#5C97DB',
55
+ '#5A7CD6',
56
+ '#6060C7',
57
+ '#674BB3',
58
+ '#693799',
59
+ '#6A277B',
60
+ '#671D60',
61
+ '#611347',
62
+ ];
63
+
64
+ /**
65
+ * From light to hard
66
+ */
67
+ const salinity = [
68
+ '#FAFAB4',
69
+ '#ECFBA1',
70
+ '#D6F598',
71
+ '#BEEB91',
72
+ '#A2E082',
73
+ '#82D67C',
74
+ '#67C77E',
75
+ '#4FB281',
76
+ '#429E8C',
77
+ '#36898F',
78
+ '#2B668F',
79
+ '#254B85',
80
+ '#213475',
81
+ '#1E1E6B',
82
+ '#1C0F5C',
83
+ ];
84
+
85
+ /**
86
+ * temperature // recommended for 5+ points
87
+ */
88
+ const sunset = [
89
+ '#FFEA80',
90
+ '#FFD971',
91
+ '#FFC171',
92
+ '#FFA76C',
93
+ '#FB8B6F',
94
+ '#EB7179',
95
+ '#D75F7F',
96
+ '#C2518D',
97
+ '#A64392',
98
+ '#8038A4',
99
+ '#6135A4',
100
+ '#4735A3',
101
+ '#283A8F',
102
+ '#013C70',
103
+ '#003752',
104
+ ];
105
+
106
+ /**
107
+ * hight contrast range // recommended for 5+ points
108
+ */
109
+ const rainbow = [
110
+ '#FFF780',
111
+ '#E7FA6F',
112
+ '#C1FA6A',
113
+ '#9BF56C',
114
+ '#79F080',
115
+ '#66E698',
116
+ '#56D7AC',
117
+ '#50C7C7',
118
+ '#56B4D7',
119
+ '#6898EB',
120
+ '#7481FA',
121
+ '#8769FA',
122
+ '#9450EB',
123
+ '#9634D6',
124
+ '#942AAE',
125
+ ];
126
+
127
+ /**
128
+ * from good to bad
129
+ */
130
+ const spectrum = [
131
+ '#43317B',
132
+ '#3B57A3',
133
+ '#3390B3',
134
+ '#5DC2B1',
135
+ '#95DBA5',
136
+ '#B9EBA0',
137
+ '#DBF5A6',
138
+ '#F5F5B7',
139
+ '#FEEA9D',
140
+ '#FFD285',
141
+ '#FA9B78',
142
+ '#E55C72',
143
+ '#C23665',
144
+ '#8F1150',
145
+ '#5C1243',
146
+ ];
147
+
148
+ /**
149
+ * from good to bad
150
+ */
151
+ const teal_red = [
152
+ '#122B5C',
153
+ '#1A496B',
154
+ '#1D7C8F',
155
+ '#21A3A3',
156
+ '#5FC7AB',
157
+ '#99E0B1',
158
+ '#CEF0CE',
159
+ '#F0F0F0',
160
+ '#FAE6D2',
161
+ '#FAC5AA',
162
+ '#FA9282',
163
+ '#E55C72',
164
+ '#C23665',
165
+ '#8F1150',
166
+ '#5C1243',
167
+ ];
168
+
169
+ /**
170
+ * Temperature // From cold → to warm
171
+ */
172
+ const blue_red = [
173
+ '#0E0E8F',
174
+ '#1D23B8',
175
+ '#3748E5',
176
+ '#647DFA',
177
+ '#96A7FA',
178
+ '#C3CCFA',
179
+ '#E1E5FA',
180
+ '#F0F0F0',
181
+ '#F9DBDB',
182
+ '#F9BDBD',
183
+ '#F59393',
184
+ '#E55C72',
185
+ '#C23665',
186
+ '#8F1150',
187
+ '#5C1243',
188
+ ];
189
+
190
+ /**
191
+ * Neutral range // from A → to B
192
+ */
193
+ const lime_rose = [
194
+ '#2E5C00',
195
+ '#49850D',
196
+ '#3748E5',
197
+ '#8FC758',
198
+ '#ABDB7B',
199
+ '#C5EBA0',
200
+ '#DCF5C4',
201
+ '#F0F0F0',
202
+ '#FADCF5',
203
+ '#F5C4ED',
204
+ '#F0A3E3',
205
+ '#E573D2',
206
+ '#CC49B6',
207
+ '#991884',
208
+ '#701260',
209
+ ];
210
+
211
+ /**
212
+ * bars only big range 7+ points // cutting
213
+ */
214
+ const viridis_magma = [
215
+ '#4A005C',
216
+ '#4A2F7F',
217
+ '#3F5895',
218
+ '#3181A0',
219
+ '#28A8A0',
220
+ '#3ECD8D',
221
+ '#86E67B',
222
+ '#CEF36C',
223
+ '#FFF680',
224
+ '#FED470',
225
+ '#FDA163',
226
+ '#F36C5A',
227
+ '#D64470',
228
+ '#A03080',
229
+ '#702084',
230
+ '#451777',
231
+ '#2B125C',
232
+ ];
233
+
234
+ export const palettes = {
235
+ viridis,
236
+ magma,
237
+ density,
238
+ salinity,
239
+ sunset,
240
+ rainbow,
241
+ spectrum,
242
+ teal_red,
243
+ blue_red,
244
+ lime_rose,
245
+ viridis_magma,
246
+ };
247
+
248
+ export type Palette = keyof typeof palettes;
249
+
250
+ /**
251
+ * Just named colors
252
+ */
253
+ export const categoricalColors = {
254
+ green_light: '#99E099',
255
+ green_bright: '#198020',
256
+ green_dark: '#42B842',
257
+ violet_light: '#C1ADFF',
258
+ violet_bright: '#845CFF',
259
+ violet_dark: '#5F31CC',
260
+ orange_light: '#FFCB8F',
261
+ orange_bright: '#FF9429',
262
+ orange_dark: '#C26A27',
263
+ teal_light: '#90E0E0',
264
+ teal_bright: '#27C2C2',
265
+ teal_dark: '#068A94',
266
+ rose_light: '#FAAAFA',
267
+ rose_bright: '#E553E5',
268
+ rose_dark: '#A324B2',
269
+ lime_light: '#CBEB67',
270
+ lime_bright: '#95C700',
271
+ lime_dark: '#659406',
272
+ blue_light: '#99CCFF',
273
+ blue_bright: '#2D93FA',
274
+ blue_dark: '#105BCC',
275
+ red_light: '#FFADBA',
276
+ red_bright: '#F05670',
277
+ red_dark: '#AD3757',
278
+ grey_light: '#D3D7E0',
279
+ grey_bright: '#929BAD',
280
+ grey_dark: '#5E5E70',
281
+ };
282
+
283
+ export type CategoricalColor = keyof typeof categoricalColors;
@@ -0,0 +1,87 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue';
3
+ import type { Color } from '@/colors';
4
+
5
+ function splitBy<T>(arr: T[], n: number): T[][] {
6
+ const res = [];
7
+ let chunk: T[] = [];
8
+
9
+ for (let i = 0; i < arr.length; i++) {
10
+ if (chunk.length < n) {
11
+ chunk.push(arr[i]);
12
+ } else {
13
+ res.push(chunk);
14
+ chunk = [arr[i]];
15
+ }
16
+ }
17
+
18
+ res.push(chunk);
19
+
20
+ return res;
21
+ }
22
+
23
+ const props = defineProps<{
24
+ maxInColumn?: number;
25
+ legends: {
26
+ color: string | Color;
27
+ text: string;
28
+ }[];
29
+ }>();
30
+
31
+ const groups = computed(() => {
32
+ return splitBy(props.legends, props.maxInColumn ?? 5);
33
+ });
34
+ </script>
35
+
36
+ <template>
37
+ <div :class="$style.component">
38
+ <div v-for="(group, k) in groups" :key="k" :class="$style.legend">
39
+ <div v-for="(l, i) in group" :key="i" :class="$style.item">
40
+ <div :class="$style.chip" :style="{ backgroundColor: l.color.toString() }" />
41
+ {{ l.text }}
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </template>
46
+
47
+ <style lang="css" module>
48
+ .component {
49
+ display: flex;
50
+ flex-direction: row;
51
+ gap: 24px;
52
+ justify-content: space-between;
53
+ }
54
+
55
+ .legend {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 4px;
59
+ flex-wrap: nowrap;
60
+ margin-top: 24px;
61
+ max-height: 120px;
62
+ flex: 1;
63
+ }
64
+
65
+ .chip {
66
+ width: 16px;
67
+ height: 16px;
68
+ border-radius: 2px;
69
+ flex-grow: 0;
70
+ }
71
+
72
+ .group {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 4px;
76
+ }
77
+
78
+ .item {
79
+ display: flex;
80
+ gap: 8px;
81
+ align-items: center;
82
+ font-size: 14px;
83
+ font-weight: 500;
84
+ line-height: 20px;
85
+ color: var(--color-txt-01);
86
+ }
87
+ </style>
@@ -0,0 +1,45 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue';
3
+ import StackedRow from './StackedRow.vue';
4
+ import Legends from './Legends.vue';
5
+ import type { PlChartStackedBarSettings } from './types';
6
+
7
+ const props = defineProps<{
8
+ settings: PlChartStackedBarSettings;
9
+ }>();
10
+
11
+ const showLegends = computed(() => props.settings.showLegends ?? true);
12
+
13
+ const data = computed(() => {
14
+ return props.settings.data ?? [];
15
+ });
16
+
17
+ const legends = computed(() => data.value.map((p) => ({
18
+ color: p.color,
19
+ text: p.label,
20
+ })));
21
+ </script>
22
+
23
+ <template>
24
+ <div :class="$style.component">
25
+ <div v-if="settings.title" :class="$style.title">{{ settings.title }}</div>
26
+ <StackedRow :value="data"/>
27
+ <Legends v-if="showLegends && legends.length" :legends="legends" :max-in-column="settings.maxLegendsInColumn" />
28
+ </div>
29
+ </template>
30
+
31
+ <style lang="scss" module>
32
+ .component {
33
+ display: flex;
34
+ flex-direction: column;
35
+ width: 100%;
36
+ }
37
+
38
+ .title {
39
+ font-size: 20px;
40
+ font-weight: 500;
41
+ line-height: 24px; /* 120% */
42
+ letter-spacing: -0.2px;
43
+ margin-bottom: 24px;
44
+ }
45
+ </style>