66from typing import Iterable , NamedTuple , Optional
77import wx
88import numpy as np
9+ from pygments import highlight
910
1011
1112class DrawMode (IntEnum ):
@@ -31,6 +32,204 @@ class DrawingInfo(NamedTuple):
3132 draw_commands : Iterable [DrawCommand ]
3233
3334
35+ def srgb_to_linear_rgb (color , as_int8 : bool = False ):
36+ color = np .asarray (color )
37+ if as_int8 :
38+ color = color / 255
39+
40+ return np .where (
41+ color <= 0.04045 ,
42+ color / 12.92 ,
43+ ((color + 0.055 ) / 1.055 ) ** 2.4
44+ )
45+
46+
47+ def linear_rgb_to_srgb (color , as_int8 : bool = False ):
48+ color = np .where (
49+ color <= 0.0031308 ,
50+ 12.92 * color ,
51+ 1.055 * color ** (1 / 2.4 ) - 0.055
52+ )
53+ return color if not as_int8 else (np .clip (color , 0 , 1 ) * 255 ).astype (np .uint8 )
54+
55+
56+ LRGB_TO_LMS = np .array ([
57+ [0.4122214708 , 0.5363325363 , 0.0514459929 ],
58+ [0.2119034982 , 0.6806995451 , 0.1073969566 ],
59+ [0.0883024619 , 0.2817188376 , 0.6299787005 ]
60+ ])
61+ LMS_TO_LRGB = np .array ([
62+ [ 4.07674166 , - 3.30771159 , 0.23096993 ],
63+ [- 1.268438 , 2.6097574 , - 0.3413194 ],
64+ [- 0.00419609 , - 0.70341861 , 1.7076147 ]
65+ ])
66+
67+ LMS_PRIME_TO_LAB = np .array ([
68+ [0.2104542553 , 0.7936177850 , - 0.0040720468 ],
69+ [1.9779984951 , - 2.4285922050 , 0.4505937099 ],
70+ [0.0259040371 , 0.7827717662 , - 0.8086757660 ],
71+ ])
72+ LAB_TO_LMS_PRIME = np .array ([
73+ [1. , 0.39633779 , 0.21580376 ],
74+ [1.00000001 , - 0.10556134 , - 0.06385417 ],
75+ [1.00000005 , - 0.08948418 , - 1.29148554 ]
76+ ])
77+
78+
79+ def linear_rgb_to_oklab (color ):
80+ return ((color @ LRGB_TO_LMS .T ) ** (1 / 3 )) @ LMS_PRIME_TO_LAB .T
81+
82+ def oklab_to_linear_rgb (color ):
83+ return ((color @ LAB_TO_LMS_PRIME .T ) ** 3 ) @ LMS_TO_LRGB .T
84+
85+ def oklab_to_oklch (color ):
86+ color = np .copy (color )
87+ color_view = np .atleast_2d (color )
88+ a , b = color_view [..., 1 :]
89+ C = np .sqrt (a * a + b * b )
90+ h = np .arctan2 (b , a )
91+ color_view [..., 1 ] = C
92+ color_view [..., 2 ] = h
93+ return color
94+
95+ def oklch_to_oklab (color ):
96+ color = np .copy (color )
97+ color_view = np .atleast_2d (color )
98+ C , h = color_view [..., 1 :]
99+ a = C * np .cos (h )
100+ b = C * np .sin (h )
101+ color_view [..., 1 ] = a
102+ color_view [..., 2 ] = b
103+ return color
104+
105+ def color_to_luminance (color ):
106+ to_y = np .array ([0.2126729 , 0.7151522 , 0.0721750 ])
107+ color = (color / 255 ) ** 2.4
108+ return np .dot (color , to_y )
109+
110+ def clamp_luminance_black_levels (y ):
111+ return np .where (y < 0.022 , np .where (y < 0 , 0 , y + (0.022 - y ) ** 1.414 ), y )
112+
113+ def apca_contrast (fg_color , bg_color ):
114+ fg_y = clamp_luminance_black_levels (color_to_luminance (fg_color ))
115+ bg_y = clamp_luminance_black_levels (color_to_luminance (bg_color ))
116+ s_apc = np .where (bg_y > fg_y , bg_y ** 0.56 - fg_y ** 0.57 , bg_y ** 0.65 - fg_y ** 0.62 ) * 1.14
117+ return np .where (np .abs (s_apc ) < 0.1 , 0.0 , np .where (s_apc < 0 , (s_apc - 0.027 ) * 100 , (s_apc + 0.027 ) * 100 ))
118+
119+
120+ def circle_line_intersection (circle , line ):
121+ pass
122+
123+
124+ def contrastify_color (fg_color , bg_color , distance : float , as_int8 : bool = False ):
125+ fg_color = linear_rgb_to_oklab (srgb_to_linear_rgb (fg_color , as_int8 ))
126+ bg_color = linear_rgb_to_oklab (srgb_to_linear_rgb (fg_color , as_int8 ))
127+ initial_distance_sq = np .sum ((fg_color - bg_color ) ** 2 , - 1 )
128+
129+ print (fg_color , bg_color )
130+ print (np .sqrt (initial_distance_sq ))
131+
132+ # We want the plane on which the hue stays the same (same angle, any lightness)
133+ plane_norm_vec = np .zeros (fg_color .shape )
134+ plane_norm_vec [..., 1 ] = - fg_color [..., 2 ]
135+ plane_norm_vec [..., 2 ] = fg_color [..., 1 ]
136+ # Normalize the vector...
137+ plane_norm_vec /= np .sqrt (np .dot (plane_norm_vec , plane_norm_vec ))
138+
139+ print (plane_norm_vec )
140+
141+ # Project background point onto the plane...
142+ bg_from_plane_delta = np .dot (bg_color , plane_norm_vec )
143+ nearest_bg_point_on_plane = bg_color - bg_from_plane_delta * plane_norm_vec
144+ remaining_distance = np .sqrt (distance * distance - bg_from_plane_delta * bg_from_plane_delta )
145+ print (bg_from_plane_delta , nearest_bg_point_on_plane )
146+
147+ fg_bg_delta = fg_color - nearest_bg_point_on_plane
148+ fg_to_bg_dist = np .sqrt (np .dot (fg_bg_delta , fg_bg_delta ))
149+ print (fg_bg_delta , fg_to_bg_dist , remaining_distance )
150+ print ()
151+ # Calculate all intersections, and direct compliments...
152+ # TODO: Actually compute 2d intersections of circle with bounds (rectangle) within valid color plane.
153+ # than pick nearest color...
154+ fg_shifted = (
155+ nearest_bg_point_on_plane + (remaining_distance / fg_to_bg_dist ) * (fg_color - nearest_bg_point_on_plane )
156+ )
157+
158+ l_bounds = (0 , 1 )
159+ ab_bounds = (- 0.5 , 0.5 )
160+
161+ print (linear_rgb_to_srgb (oklab_to_linear_rgb (np .where (fg_to_bg_dist < remaining_distance , fg_shifted , fg_color )), as_int8 ))
162+ return linear_rgb_to_srgb (oklab_to_linear_rgb (np .where (fg_to_bg_dist < remaining_distance , fg_shifted , fg_color )), as_int8 )
163+
164+
165+ class WxPlotStyles :
166+ def __init__ (self , widget : wx .Control , min_color_dist : float = 0.5 , accented_alpha : float = 0.3 ):
167+ self .background_color = widget .GetBackgroundColour ()
168+ self .foreground_color = widget .GetForegroundColour ()
169+
170+ def apply_apca (fg , bg , desired_val ):
171+ #return fg
172+ return wx .Colour (* contrastify_color (fg [:3 ], bg [:3 ], desired_val , True ), 255 )
173+
174+ def alpha_shift (color , salpha ):
175+ return wx .Colour (* color [:3 ], int (color .Alpha () * salpha ))
176+
177+ self .highlight_color = apply_apca (
178+ wx .SystemSettings .GetColour (wx .SYS_COLOUR_HIGHLIGHT ),
179+ self .background_color ,
180+ min_color_dist
181+ )
182+ self .highlight_color2 = alpha_shift (self .highlight_color , accented_alpha )
183+
184+ # WX widgets doesn't provide an error highlight color. Since the
185+ # highlight color doesn't typically match the foreground or
186+ # background, we take the complement of it as a second selection color
187+ # (This color happens to usually be a Blue, so this typically produces
188+ # a Red/Orange)
189+ self .error_color = apply_apca (
190+ wx .Colour (* (255 - np .asarray (self .highlight_color [:3 ])), self .highlight_color .Alpha ()),
191+ self .background_color ,
192+ min_color_dist
193+ )
194+ self .error_color2 = alpha_shift (self .error_color , accented_alpha )
195+
196+ self .fixed_error_color = apply_apca (
197+ wx .Colour (* (((
198+ np .asarray (self .background_color , int )
199+ + np .asarray (self .foreground_color , int )
200+ ) / 2 ).astype (int ))),
201+ self .background_color ,
202+ min_color_dist
203+ )
204+ self .fixed_error_color2 = alpha_shift (self .fixed_error_color , accented_alpha )
205+
206+ # All the pens and brushes we will need...
207+ self .transparent_pen = wx .Pen (self .highlight_color , 2 , wx .PENSTYLE_TRANSPARENT )
208+
209+ # Primary highlight color (normal plot locations)...
210+ self .highlight_pen = wx .Pen (self .highlight_color , 2 , wx .PENSTYLE_SOLID )
211+ self .highlight_pen2 = wx .Pen (self .highlight_color , 5 , wx .PENSTYLE_SOLID )
212+ self .highlight_brush = wx .Brush (self .highlight_color2 , wx .BRUSHSTYLE_SOLID )
213+
214+ self .error_pen = wx .Pen (self .error_color , 2 , wx .PENSTYLE_SOLID )
215+ self .error_pen2 = wx .Pen (self .error_color , 5 , wx .PENSTYLE_SOLID )
216+ self .error_brush = wx .Brush (self .error_color2 , wx .BRUSHSTYLE_SOLID )
217+
218+ self .fixed_error_pen = wx .Pen (self .fixed_error_color , 2 , wx .PENSTYLE_SOLID )
219+ self .fixed_error_pen2 = wx .Pen (self .fixed_error_color , 5 , wx .PENSTYLE_SOLID )
220+ self .fixed_error_brush = wx .Brush (self .fixed_error_color2 , wx .BRUSHSTYLE_SOLID )
221+
222+ self .indicator_brush = wx .Brush (self .foreground_color , wx .BRUSHSTYLE_SOLID )
223+ self .indicator_pen = wx .Pen (self .foreground_color , 5 , wx .PENSTYLE_SOLID )
224+ self .indicator_pen2 = wx .Pen (self .foreground_color , 1 , wx .PENSTYLE_SOLID )
225+
226+ def is_valid (self , widget : wx .Control ):
227+ return (
228+ tuple (self .background_color [:3 ]) == tuple (widget .GetBackgroundColour ()[:3 ])
229+ and tuple (self .foreground_color [:3 ]) == tuple (widget .GetForegroundColour ()[:3 ])
230+ )
231+
232+
34233class ProbabilityDisplayer (wx .Control ):
35234 """
36235 A custom wx.Control which displays a list of probabilities in the form of a line segment plot. Uses native colors
@@ -90,6 +289,8 @@ def __init__(
90289 self ._segment_starts = None
91290 self ._segment_fix_frames = None
92291
292+ self ._styles = None
293+
93294 self ._best_size = wx .Size (self .MIN_PROB_STEP * 5 , max (height , (self .TRIANGLE_SIZE * 4 ) + self .TOP_PADDING ))
94295 self .SetMinSize (self ._best_size )
95296 self .SetInitialSize (self ._best_size )
@@ -248,104 +449,61 @@ def on_draw(self, dc: wx.DC):
248449 if ((not width ) or (not height )):
249450 return
250451
452+ if (self ._styles is not None and not self ._styles .is_valid (self )):
453+ self ._styles = None
454+ if (self ._styles is None ):
455+ self ._styles = WxPlotStyles (self )
456+ s = self ._styles
457+
251458 # Clear the background with the default color...
252459 dc .SetBackground (
253- wx .Brush (self . GetBackgroundColour () , wx .BRUSHSTYLE_SOLID )
460+ wx .Brush (s . background_color , wx .BRUSHSTYLE_SOLID )
254461 )
255462 dc .Clear ()
256463
257- # Colors used in pens and brushes below...
258- highlight_color = wx .SystemSettings .GetColour (wx .SYS_COLOUR_HIGHLIGHT )
259- highlight_color2 = wx .Colour (
260- * highlight_color [:3 ],
261- int (highlight_color .Alpha () * 0.3 )
262- )
263- # WX widgets doesn't provide an error highlight color. Since the
264- # highlight color doesn't typically match the foreground or
265- # background, we take the complement of it as a second selection color
266- # (This color happens to usually be a Blue, so this typically produces
267- # a Red/Orange)
268- error_color = wx .Colour (
269- 255 - highlight_color .Red (),
270- 255 - highlight_color .Green (),
271- 255 - highlight_color .Blue (),
272- highlight_color .Alpha ()
273- )
274- error_color2 = wx .Colour (* error_color [:3 ], int (error_color .Alpha () * 0.3 ))
275-
276- foreground_color = self .GetForegroundColour ()
277-
278- fixed_error_color = wx .Colour (* (((
279- np .asarray (self .GetBackgroundColour (), int )
280- + np .asarray (foreground_color , int )
281- ) / 2 ).astype (int )))
282- fixed_error_color2 = wx .Colour (
283- * fixed_error_color [:3 ], int (fixed_error_color .Alpha () * 0.3 )
284- )
285-
286- # All the pens and brushes we will need...
287- transparent_pen = wx .Pen (highlight_color , 2 , wx .PENSTYLE_TRANSPARENT )
288-
289- # Primary highlight color (normal plot locations)...
290- highlight_pen = wx .Pen (highlight_color , 2 , wx .PENSTYLE_SOLID )
291- highlight_pen2 = wx .Pen (highlight_color , 5 , wx .PENSTYLE_SOLID )
292- highlight_brush = wx .Brush (highlight_color2 , wx .BRUSHSTYLE_SOLID )
293-
294- error_pen = wx .Pen (error_color , 2 , wx .PENSTYLE_SOLID )
295- error_pen2 = wx .Pen (error_color , 5 , wx .PENSTYLE_SOLID )
296- error_brush = wx .Brush (error_color2 , wx .BRUSHSTYLE_SOLID )
297-
298- fixed_error_pen = wx .Pen (fixed_error_color , 2 , wx .PENSTYLE_SOLID )
299- fixed_error_pen2 = wx .Pen (fixed_error_color , 5 , wx .PENSTYLE_SOLID )
300- fixed_error_brush = wx .Brush (fixed_error_color2 , wx .BRUSHSTYLE_SOLID )
301-
302- indicator_brush = wx .Brush (foreground_color , wx .BRUSHSTYLE_SOLID )
303- indicator_pen = wx .Pen (foreground_color , 5 , wx .PENSTYLE_SOLID )
304- indicator_pen2 = wx .Pen (foreground_color , 1 , wx .PENSTYLE_SOLID )
305-
306464 # This patches the point drawing for the latest versions of wxWidgets, which don't respect the pen's width correctly...
307465 def draw_points (points , pen ):
308466 top = np .round ((np .asarray (points ) - pen .GetWidth () / 2 )).astype (int )
309467 args = np .concatenate ([top , np .full (top .shape , pen .GetWidth (), dtype = int )], axis = - 1 )
310- dc .DrawEllipseList (args , transparent_pen , wx .Brush (pen .GetColour (), wx .BRUSHSTYLE_SOLID ))
468+ dc .DrawEllipseList (args , s . transparent_pen , wx .Brush (pen .GetColour (), wx .BRUSHSTYLE_SOLID ))
311469
312470 # Compute the center and points to place on the line...
313471 draw_info = self ._compute_points (height , width )
314472
315473 for seg_x in draw_info .segment_xs :
316474 seg_x = int (seg_x )
317- dc .DrawLineList ([[seg_x , 0 , seg_x , height ]], fixed_error_pen )
475+ dc .DrawLineList ([[seg_x , 0 , seg_x , height ]], s . fixed_error_pen )
318476 dc .DrawPolygonList ([[
319477 [seg_x - int (self .TRIANGLE_SIZE / 2 ), 0 ],
320478 [seg_x + int (self .TRIANGLE_SIZE / 2 ), 0 ],
321479 [seg_x , int (self .TRIANGLE_SIZE )]
322- ]], fixed_error_pen , fixed_error_brush )
480+ ]], s . fixed_error_pen , s . fixed_error_brush )
323481
324482 for seg_x in draw_info .segment_fix_xs :
325483 seg_x = int (seg_x )
326484 dc .DrawPolygonList ([[
327485 [seg_x - int (self .TRIANGLE_SIZE / 2 ), 0 ],
328486 [seg_x + int (self .TRIANGLE_SIZE / 2 ), 0 ],
329487 [seg_x , int (self .TRIANGLE_SIZE )]
330- ]], indicator_pen2 , highlight_brush )
488+ ]], s . indicator_pen2 , s . highlight_brush )
331489
332490 # Plot all of the points the filled-in polygon underneath, and the line connecting the points...
333491 for draw_command in draw_info .draw_commands :
334492 if (draw_command .draw_mode == DrawMode .USER_MODIFIED ):
335493 continue
336494
337495 if (draw_command .draw_mode == DrawMode .NORMAL ):
338- pen = highlight_pen
339- pen2 = highlight_pen2
340- brush = highlight_brush
496+ pen = s . highlight_pen
497+ pen2 = s . highlight_pen2
498+ brush = s . highlight_brush
341499 elif (draw_command .draw_mode == DrawMode .POORLY_LABELED ):
342- pen = error_pen
343- pen2 = error_pen2
344- brush = error_brush
500+ pen = s . error_pen
501+ pen2 = s . error_pen2
502+ brush = s . error_brush
345503 else :
346- pen = fixed_error_pen
347- pen2 = fixed_error_pen2
348- brush = fixed_error_brush
504+ pen = s . fixed_error_pen
505+ pen2 = s . fixed_error_pen2
506+ brush = s . fixed_error_brush
349507
350508 poly_begin_point = (draw_command .points [0 ] + draw_command .point_before ) / 2
351509 poly_end_point = (draw_command .points [- 1 ] + draw_command .point_after ) / 2
@@ -359,7 +517,7 @@ def draw_points(points, pen):
359517
360518 dc .DrawPolygonList (
361519 [np .concatenate ((draw_command .points , wrap_polygon_points ))],
362- transparent_pen ,
520+ s . transparent_pen ,
363521 brush
364522 )
365523
@@ -370,21 +528,21 @@ def draw_points(points, pen):
370528 draw_points (draw_command .points .astype (int ), pen2 )
371529
372530 # Draw the current location indicating line, point and arrow, indicates which data point we are currently on.
373- dc .DrawLineList ([[int (draw_info .x_center ), 0 , int (draw_info .x_center ), height ]], indicator_pen2 )
531+ dc .DrawLineList ([[int (draw_info .x_center ), 0 , int (draw_info .x_center ), height ]], s . indicator_pen2 )
374532 dc .DrawPolygonList ([[
375533 [int (draw_info .x_center - self .TRIANGLE_SIZE ), height ],
376534 [int (draw_info .x_center + self .TRIANGLE_SIZE ), height ],
377535 [int (draw_info .x_center ), height - int (self .TRIANGLE_SIZE * 1.5 )]
378- ]], indicator_pen2 , indicator_brush )
536+ ]], s . indicator_pen2 , s . indicator_brush )
379537 if (draw_info .center_draw_mode != DrawMode .USER_MODIFIED ):
380- draw_points ([[int (draw_info .x_center ), int (draw_info .y_center )]], indicator_pen )
538+ draw_points ([[int (draw_info .x_center ), int (draw_info .y_center )]], s . indicator_pen )
381539
382540 # If the user set the name of this probability display plot, write it to the top-left corner...
383541 if (self ._text is not None ):
384- back_pen = wx .Pen (self . GetBackgroundColour () , 3 , wx .PENSTYLE_SOLID )
385- back_brush = wx .Brush (self . GetBackgroundColour () , wx .BRUSHSTYLE_SOLID )
386- dc .SetTextBackground (self . GetBackgroundColour () )
387- dc .SetTextForeground (self . GetForegroundColour () )
542+ back_pen = wx .Pen (s . background_color , 3 , wx .PENSTYLE_SOLID )
543+ back_brush = wx .Brush (s . background_color , wx .BRUSHSTYLE_SOLID )
544+ dc .SetTextBackground (s . background_color )
545+ dc .SetTextForeground (s . foreground_color )
388546
389547 dc .SetFont (self .GetFont ())
390548 size : wx .Size = dc .GetTextExtent (self ._text )
0 commit comments