oscura 0.8.0__py3-none-any.whl → 0.11.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.
- oscura/__init__.py +19 -19
- oscura/__main__.py +4 -0
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/validation.py +17 -10
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- oscura-0.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/colors.py
DELETED
|
@@ -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
|
-
]
|