oscura 0.7.0__py3-none-any.whl → 0.10.0__py3-none-any.whl

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.
Files changed (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,451 +0,0 @@
1
- """Color palette selection and accessibility utilities.
2
-
3
- This module provides intelligent color palette selection based on data
4
- characteristics and accessibility requirements with WCAG contrast checking.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.colors import select_optimal_palette
9
- >>> colors = select_optimal_palette(n_channels=3, palette_type="qualitative")
10
-
11
- References:
12
- WCAG 2.1 contrast guidelines
13
- Colorblind-safe palette design (Brettel 1997)
14
- ColorBrewer schemes
15
- """
16
-
17
- from typing import Literal
18
-
19
- import numpy as np
20
-
21
- # Predefined colorblind-safe palettes
22
- COLORBLIND_SAFE_QUALITATIVE = [
23
- "#0173B2", # Blue
24
- "#DE8F05", # Orange
25
- "#029E73", # Green
26
- "#CC78BC", # Purple
27
- "#CA9161", # Brown
28
- "#949494", # Gray
29
- "#ECE133", # Yellow
30
- "#56B4E9", # Light blue
31
- ]
32
-
33
- SEQUENTIAL_VIRIDIS = [
34
- "#440154",
35
- "#481567",
36
- "#482677",
37
- "#453781",
38
- "#404788",
39
- "#39568C",
40
- "#33638D",
41
- "#2D708E",
42
- "#287D8E",
43
- "#238A8D",
44
- "#1F968B",
45
- "#20A387",
46
- "#29AF7F",
47
- "#3CBB75",
48
- "#55C667",
49
- "#73D055",
50
- "#95D840",
51
- "#B8DE29",
52
- "#DCE319",
53
- "#FDE724",
54
- ]
55
-
56
- DIVERGING_COOLWARM = [
57
- "#3B4CC0",
58
- "#5977E3",
59
- "#7D9EF2",
60
- "#A2C0F9",
61
- "#C7DDFA",
62
- "#E8F0FC",
63
- "#F9EBE5",
64
- "#F6CFBB",
65
- "#F0AD8E",
66
- "#E68462",
67
- "#D8583E",
68
- "#C52A1E",
69
- "#B40426",
70
- ]
71
-
72
-
73
- def select_optimal_palette(
74
- n_colors: int,
75
- *,
76
- palette_type: Literal["sequential", "diverging", "qualitative"] | None = None,
77
- data_range: tuple[float, float] | None = None,
78
- colorblind_safe: bool = True,
79
- background_color: str = "#FFFFFF",
80
- min_contrast_ratio: float = 4.5,
81
- ) -> list[str]:
82
- """Select optimal color palette based on data characteristics.
83
-
84
- : Automatically select optimal color palettes based on
85
- data characteristics, plot type, and accessibility requirements.
86
-
87
- Args:
88
- n_colors: Number of colors needed
89
- palette_type: Type of palette ("sequential", "diverging", "qualitative")
90
- If None, auto-select based on n_colors and data_range
91
- data_range: Data range (min, max) for auto-detecting bipolar signals
92
- colorblind_safe: Ensure colorblind-safe palette (default: True)
93
- background_color: Background color for contrast checking (default: white)
94
- min_contrast_ratio: Minimum WCAG contrast ratio (default: 4.5 for AA)
95
-
96
- Returns:
97
- List of color hex codes
98
-
99
- Raises:
100
- ValueError: If n_colors is invalid or palette cannot meet requirements
101
-
102
- Example:
103
- >>> # Auto-select for 3 channels
104
- >>> colors = select_optimal_palette(3)
105
- >>> # Diverging palette for bipolar data
106
- >>> colors = select_optimal_palette(10, palette_type="diverging")
107
-
108
- References:
109
- VIS-023: Data-Driven Color Palette
110
- WCAG 2.1 contrast ratio guidelines (AA: 4.5:1, AAA: 7:1)
111
- ColorBrewer sequential/diverging schemes
112
- """
113
- if n_colors < 1:
114
- raise ValueError("n_colors must be >= 1")
115
- if min_contrast_ratio < 1.0:
116
- raise ValueError("min_contrast_ratio must be >= 1.0")
117
-
118
- # Auto-select palette type if not specified
119
- if palette_type is None:
120
- palette_type = _auto_select_palette_type(n_colors, data_range)
121
-
122
- # Select base palette
123
- if palette_type == "qualitative":
124
- base_colors = (
125
- COLORBLIND_SAFE_QUALITATIVE if colorblind_safe else _generate_qualitative(n_colors)
126
- )
127
- elif palette_type == "sequential":
128
- base_colors = SEQUENTIAL_VIRIDIS
129
- elif palette_type == "diverging":
130
- base_colors = DIVERGING_COOLWARM
131
- else:
132
- raise ValueError(f"Unknown palette_type: {palette_type}")
133
-
134
- # Sample colors if we need fewer than available
135
- if n_colors <= len(base_colors):
136
- # Evenly sample from palette
137
- indices = np.linspace(0, len(base_colors) - 1, n_colors).astype(int)
138
- colors = [base_colors[i] for i in indices]
139
- else:
140
- # Interpolate if we need more colors
141
- colors = _interpolate_colors(base_colors, n_colors)
142
-
143
- # Check contrast ratios
144
- colors_with_contrast = []
145
- bg_luminance = _relative_luminance(background_color)
146
-
147
- for color in colors:
148
- color_luminance = _relative_luminance(color)
149
- contrast = _contrast_ratio(color_luminance, bg_luminance)
150
-
151
- if contrast >= min_contrast_ratio:
152
- colors_with_contrast.append(color)
153
- else:
154
- # Adjust lightness to meet contrast requirement
155
- adjusted = _adjust_for_contrast(color, background_color, min_contrast_ratio)
156
- colors_with_contrast.append(adjusted)
157
-
158
- return colors_with_contrast
159
-
160
-
161
- def _auto_select_palette_type(
162
- n_colors: int,
163
- data_range: tuple[float, float] | None,
164
- ) -> Literal["sequential", "diverging", "qualitative"]:
165
- """Auto-select palette type based on data characteristics.
166
-
167
- Args:
168
- n_colors: Number of colors needed
169
- data_range: Data range (min, max)
170
-
171
- Returns:
172
- Palette type
173
- """
174
- # Check for bipolar data (zero-crossing)
175
- if data_range is not None:
176
- min_val, max_val = data_range
177
- if min_val < 0 and max_val > 0:
178
- # Bipolar signal - use diverging
179
- return "diverging"
180
-
181
- # Multi-channel (distinct categories)
182
- if n_colors <= 8:
183
- return "qualitative"
184
-
185
- # Many colors or continuous data
186
- return "sequential"
187
-
188
-
189
- def _relative_luminance(color: str) -> float:
190
- """Calculate relative luminance per WCAG 2.1.
191
-
192
- Args:
193
- color: Hex color code
194
-
195
- Returns:
196
- Relative luminance (0-1)
197
- """
198
- # Parse hex color
199
- color = color.removeprefix("#")
200
-
201
- r = int(color[0:2], 16) / 255.0
202
- g = int(color[2:4], 16) / 255.0
203
- b = int(color[4:6], 16) / 255.0
204
-
205
- # Convert to linear RGB
206
- def to_linear(c: float) -> float:
207
- if c <= 0.03928:
208
- return c / 12.92
209
- else:
210
- return ((c + 0.055) / 1.055) ** 2.4 # type: ignore[no-any-return]
211
-
212
- r_linear = to_linear(r)
213
- g_linear = to_linear(g)
214
- b_linear = to_linear(b)
215
-
216
- # Calculate luminance
217
- return 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear
218
-
219
-
220
- def _contrast_ratio(lum1: float, lum2: float) -> float:
221
- """Calculate WCAG contrast ratio between two luminances.
222
-
223
- Args:
224
- lum1: First luminance (0-1)
225
- lum2: Second luminance (0-1)
226
-
227
- Returns:
228
- Contrast ratio (1-21)
229
- """
230
- lighter = max(lum1, lum2)
231
- darker = min(lum1, lum2)
232
-
233
- return (lighter + 0.05) / (darker + 0.05)
234
-
235
-
236
- def _adjust_for_contrast(
237
- color: str,
238
- background: str,
239
- target_ratio: float,
240
- ) -> str:
241
- """Adjust color lightness to meet contrast requirement.
242
-
243
- Args:
244
- color: Color to adjust
245
- background: Background color
246
- target_ratio: Target contrast ratio
247
-
248
- Returns:
249
- Adjusted color hex code
250
- """
251
- # Parse color
252
- color_val = color.removeprefix("#")
253
-
254
- r = int(color_val[0:2], 16)
255
- g = int(color_val[2:4], 16)
256
- b = int(color_val[4:6], 16)
257
-
258
- # Convert to HSL for easier lightness adjustment
259
- h, s, l = _rgb_to_hsl(r, g, b)
260
-
261
- bg_lum = _relative_luminance(background)
262
-
263
- # Binary search for appropriate lightness
264
- l_min, l_max = 0.0, 1.0
265
- iterations = 0
266
- max_iterations = 20
267
-
268
- while iterations < max_iterations:
269
- # Try current lightness
270
- test_r, test_g, test_b = _hsl_to_rgb(h, s, l)
271
- test_color = f"#{test_r:02x}{test_g:02x}{test_b:02x}"
272
- test_lum = _relative_luminance(test_color)
273
- ratio = _contrast_ratio(test_lum, bg_lum)
274
-
275
- if abs(ratio - target_ratio) < 0.1:
276
- break
277
-
278
- if ratio < target_ratio:
279
- # Need more contrast - adjust lightness
280
- if bg_lum > 0.5:
281
- # Dark background - make lighter
282
- l_min = l
283
- l = (l + l_max) / 2
284
- else:
285
- # Light background - make darker
286
- l_max = l
287
- l = (l_min + l) / 2
288
- # Too much contrast - move back
289
- elif bg_lum > 0.5:
290
- l_max = l
291
- l = (l_min + l) / 2
292
- else:
293
- l_min = l
294
- l = (l + l_max) / 2
295
-
296
- iterations += 1
297
-
298
- final_r, final_g, final_b = _hsl_to_rgb(h, s, l)
299
- return f"#{final_r:02x}{final_g:02x}{final_b:02x}"
300
-
301
-
302
- def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
303
- """Convert RGB to HSL color space.
304
-
305
- Args:
306
- r: Red value (0-255).
307
- g: Green value (0-255).
308
- b: Blue value (0-255).
309
-
310
- Returns:
311
- (h, s, l) tuple where h in [0, 360), s and l in [0, 1]
312
- """
313
- r_norm = r / 255.0
314
- g_norm = g / 255.0
315
- b_norm = b / 255.0
316
-
317
- max_c = max(r_norm, g_norm, b_norm)
318
- min_c = min(r_norm, g_norm, b_norm)
319
- delta = max_c - min_c
320
-
321
- # Lightness
322
- l = (max_c + min_c) / 2.0
323
-
324
- if delta == 0:
325
- # Achromatic
326
- return (0.0, 0.0, l)
327
-
328
- # Saturation
329
- s = delta / (max_c + min_c) if l < 0.5 else delta / (2.0 - max_c - min_c)
330
-
331
- # Hue
332
- if max_c == r_norm:
333
- h = ((g_norm - b_norm) / delta) % 6
334
- elif max_c == g_norm:
335
- h = ((b_norm - r_norm) / delta) + 2
336
- else:
337
- h = ((r_norm - g_norm) / delta) + 4
338
-
339
- h = h * 60.0
340
-
341
- return (h, s, l)
342
-
343
-
344
- def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]:
345
- """Convert HSL to RGB color space.
346
-
347
- Args:
348
- h: Hue in [0, 360)
349
- s: Saturation in [0, 1]
350
- l: Lightness in [0, 1]
351
-
352
- Returns:
353
- (r, g, b) tuple with values in [0, 255]
354
- """
355
- if s == 0:
356
- # Achromatic
357
- gray = int(l * 255)
358
- return (gray, gray, gray)
359
-
360
- def hue_to_rgb(p: float, q: float, t: float) -> float:
361
- if t < 0:
362
- t += 1
363
- if t > 1:
364
- t -= 1
365
- if t < 1 / 6:
366
- return p + (q - p) * 6 * t
367
- if t < 1 / 2:
368
- return q
369
- if t < 2 / 3:
370
- return p + (q - p) * (2 / 3 - t) * 6
371
- return p
372
-
373
- q = l * (1 + s) if l < 0.5 else l + s - l * s
374
-
375
- p = 2 * l - q
376
-
377
- h_norm = h / 360.0
378
-
379
- r = hue_to_rgb(p, q, h_norm + 1 / 3)
380
- g = hue_to_rgb(p, q, h_norm)
381
- b = hue_to_rgb(p, q, h_norm - 1 / 3)
382
-
383
- return (int(r * 255), int(g * 255), int(b * 255))
384
-
385
-
386
- def _generate_qualitative(n_colors: int) -> list[str]:
387
- """Generate qualitative color palette.
388
-
389
- Args:
390
- n_colors: Number of colors
391
-
392
- Returns:
393
- List of hex color codes
394
- """
395
- # Generate evenly spaced hues
396
- colors = []
397
- for i in range(n_colors):
398
- hue = (i * 360.0 / n_colors) % 360
399
- r, g, b = _hsl_to_rgb(hue, 0.7, 0.5)
400
- colors.append(f"#{r:02x}{g:02x}{b:02x}")
401
-
402
- return colors
403
-
404
-
405
- def _interpolate_colors(base_colors: list[str], n_colors: int) -> list[str]:
406
- """Interpolate between base colors to generate more colors.
407
-
408
- Args:
409
- base_colors: Base color palette
410
- n_colors: Target number of colors
411
-
412
- Returns:
413
- List of interpolated hex color codes
414
- """
415
- if n_colors <= len(base_colors):
416
- return base_colors[:n_colors]
417
-
418
- # Convert to RGB arrays
419
- rgb_array = np.zeros((len(base_colors), 3))
420
- for i, color in enumerate(base_colors):
421
- color = color.removeprefix("#")
422
- rgb_array[i, 0] = int(color[0:2], 16)
423
- rgb_array[i, 1] = int(color[2:4], 16)
424
- rgb_array[i, 2] = int(color[4:6], 16)
425
-
426
- # Interpolate
427
- indices = np.linspace(0, len(base_colors) - 1, n_colors)
428
- interp_rgb = np.zeros((n_colors, 3))
429
-
430
- for channel in range(3):
431
- interp_rgb[:, channel] = np.interp(
432
- indices, np.arange(len(base_colors)), rgb_array[:, channel]
433
- )
434
-
435
- # Convert back to hex
436
- colors = []
437
- for i in range(n_colors):
438
- r = int(interp_rgb[i, 0])
439
- g = int(interp_rgb[i, 1])
440
- b = int(interp_rgb[i, 2])
441
- colors.append(f"#{r:02x}{g:02x}{b:02x}")
442
-
443
- return colors
444
-
445
-
446
- __all__ = [
447
- "COLORBLIND_SAFE_QUALITATIVE",
448
- "DIVERGING_COOLWARM",
449
- "SEQUENTIAL_VIRIDIS",
450
- "select_optimal_palette",
451
- ]