plot-io 0.12.0__py3-none-win_amd64.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 (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-x86_64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,295 @@
1
+ """Color values: the ``color`` validator, named color scales, references to the plot's colors
2
+ (``"C0"``) and operations on colors (alpha, saturation, luminance).
3
+
4
+ Named color scales: ``"[variant] name [level]"``: ``"blue"``, ``"blue 0.45"``,
5
+ ``"pastel blue"``, ``"pastel blue 45%"``.
6
+
7
+ Each name has eleven shades: levels 0.05 and 0.1 to 0.9 and 0.95 (Tailwind CSS 4's 50,
8
+ 100 to 900 and 950, in OKLCh); level 0 is white and level 1 black. A level between two
9
+ shades is interpolated in Oklab; the color is then gamut mapped into sRGB as CSS Color 4
10
+ specifies (vec_io's ``oklab()``). A bare name is its level 0.5. These names take
11
+ precedence over CSS's (``"red"`` is Red 0.5, not ``#ff0000``; ``"brown"`` is Brown 0.5,
12
+ not ``#a52a2a``); other CSS names (``"navy"``) are vec_io's.
13
+
14
+ Variants (:data:`VARIANTS`): ``base`` (Tailwind's), ``vivid``, ``pastel``, ``forest``,
15
+ ``dim``, ``arctic``, ``desert``, ``dusty``, ``neon``. A name without a variant word takes
16
+ the ``color_variant`` in effect where it is drawn (:class:`Named`, :func:`variant_of`).
17
+ """
18
+
19
+ import colorsys
20
+ import functools
21
+ import re
22
+ from typing import Any
23
+
24
+ from vec_io.paint import CMYK, as_rgba, oklch_to_oklab, parse_color, srgb_to_oklab, to_rgba
25
+
26
+ from .. import hints
27
+ from ..units import Fixed
28
+ from ..validators import Color, typed
29
+ from .variants import TABLES, VARIANTS
30
+
31
+ __all__ = ["SCALES", "VARIANTS", "VARIANT_DOC", "Named", "color", "split_colors", "scale_color", "scale_names",
32
+ "resolve_color", "variant_of", "check_variant", "with_alpha", "desaturate",
33
+ "luminance", "ink_for", "nearest_named"]
34
+
35
+ #: The shades of each scale of the base variant (levels 0.05, 0.1–0.9, 0.95: Tailwind's 50,
36
+ #: 100–900, 950) as OKLCh: lightness in %, chroma, hue in degrees.
37
+ SCALES = {**TABLES["base"], "black": ((0, 0, 0),) * 11, "white": ((100, 0, 0),) * 11}
38
+ _TABLES = {v: {**TABLES[v], "black": SCALES["black"], "white": SCALES["white"]}
39
+ for v in VARIANTS}
40
+ _ALIASES = {"grey": "gray"}
41
+ _RE = re.compile(r"^\s*(?:([a-z]+)\s+)?([a-z]+)(?:\s+(\d*\.?\d+(?:e[-+]?\d+)?)(%?))?\s*$", re.I)
42
+ #: Stop positions: white, the eleven shades, black.
43
+ _STOPS = (0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1.0)
44
+
45
+
46
+ def scale_names() -> list[str]:
47
+ return sorted(SCALES)
48
+
49
+
50
+ #: The doc of the ``color_variant`` settings.
51
+ VARIANT_DOC = ("Variant of the named colors written without one (`\"red\"`, color maps from names, "
52
+ "the style's colors): `base`, `vivid`, `pastel`, "
53
+ "`forest`, `dim`, `arctic`, `desert`, `dusty`, `neon`.")
54
+
55
+
56
+ @typed("Literal[" + ", ".join(map(repr, VARIANTS)) + "]")
57
+ def check_variant(v: Any) -> str:
58
+ """A variant word (``"pastel"``); ``ValueError`` naming the nine otherwise."""
59
+ if isinstance(v, str) and v.strip().lower() in VARIANTS:
60
+ return v.strip().lower()
61
+ raise ValueError(f"unknown color variant {v!r} (variants: {', '.join(VARIANTS)})")
62
+
63
+
64
+ def _oklab(name: str, i: int, variant: str = "base") -> tuple[float, float, float]:
65
+ """Oklab of stop ``i`` of a scale (:data:`_STOPS`): 0 white, 1–11 the shades, 12
66
+ black."""
67
+ if i == 0:
68
+ return (1.0, 0.0, 0.0)
69
+ if i == 12:
70
+ return (0.0, 0.0, 0.0)
71
+ L, C, H = _TABLES[variant][name][i - 1]
72
+ return oklch_to_oklab(L / 100, C, H)
73
+
74
+
75
+ @functools.lru_cache(maxsize=4096)
76
+ def _hex(name: str, level: float, variant: str = "base") -> str:
77
+ if 0.1 <= level <= 0.9: # between shades 100 and 900
78
+ pos = level * 10
79
+ i = min(int(pos), 9)
80
+ t = pos - i
81
+ i += 1
82
+ else: # white, 50, 100 or 900, 950, black
83
+ pos = (level if level < 0.1 else level - 0.9) * 20
84
+ i = min(int(pos), 1)
85
+ t = pos - i
86
+ if level > 0.9:
87
+ i += 10
88
+ a, b = _oklab(name, i, variant), _oklab(name, i + 1, variant)
89
+ if t < 1e-9 or t > 1 - 1e-9:
90
+ t = round(t)
91
+ lab = [x + (y - x) * t for x, y in zip(a, b)]
92
+ color = parse_color("oklab({!r} {!r} {!r})".format(*lab))
93
+ if color is None: # not reached: valid oklab()
94
+ raise ValueError(f"no color for {name} {level}")
95
+ return "#" + "".join(f"{round(v * 255):02x}" for v in as_rgba(color)[:3])
96
+
97
+
98
+ def rgba_of(c: Any) -> tuple[float, ...] | None:
99
+ """``(r, g, b, a)`` of a color (a print color's RGB approximation); ``None`` for none."""
100
+ r = to_rgba(c)
101
+ return None if r is None else as_rgba(r)
102
+
103
+
104
+ def nearest_named(c: Any, level: float | None = None, variant: str = "base") -> Any:
105
+ """The named color closest to ``c`` (in Oklab) in ``variant``: the nearest of the
106
+ shades 100 to 900 (levels 0.1–0.9), or with ``level``, the scale of the nearest shade
107
+ at that level. Colors vec_io cannot read (``None``, CMYK) are returned as given; alpha
108
+ is kept."""
109
+ if c is None or isinstance(c, CMYK):
110
+ return c
111
+ rgba = rgba_of(c)
112
+ if rgba is None:
113
+ return c
114
+ near = _nearest(tuple(round(v, 6) for v in rgba[:3]), level, variant)
115
+ return near if len(rgba) < 4 or rgba[3] >= 1 else with_alpha(near, rgba[3])
116
+
117
+
118
+ @functools.lru_cache(maxsize=4096)
119
+ def _nearest(rgb: tuple[float, ...], level: float | None, variant: str) -> str:
120
+ lab = srgb_to_oklab(*rgb)
121
+ best = (float("inf"), "", 0)
122
+ for name in SCALES:
123
+ for i in range(1, 10):
124
+ d = sum((x - y) ** 2 for x, y in zip(lab, _oklab(name, i + 1, variant)))
125
+ if d < best[0]:
126
+ best = (d, name, i)
127
+ _, name, i = best
128
+ return _hex(name, i / 10 if level is None else level, variant)
129
+
130
+
131
+ class Named(Fixed):
132
+ """A named color as checked: ``source`` the text given, ``value`` its hex in its own
133
+ variant (``variant``, or base when it has none); a name without a variant word takes
134
+ the variant in effect where it is drawn (:meth:`in_variant`)."""
135
+
136
+ __slots__ = ("name", "level", "variant")
137
+
138
+ def __init__(self, source: str, name: str, level: float, variant: str | None) -> None:
139
+ super().__init__(source, _hex(name, level, variant or "base"))
140
+ self.name, self.level, self.variant = name, level, variant
141
+
142
+ def in_variant(self, variant: str) -> str:
143
+ """The hex in ``variant``, unless the name has its own."""
144
+ if self.variant is not None or variant == "base":
145
+ return self.value
146
+ return _hex(self.name, self.level, variant)
147
+
148
+ def in_context(self, obj: Any) -> str:
149
+ return self.value if self.variant is not None else self.in_variant(variant_of(obj))
150
+
151
+ @property
152
+ def varies(self) -> bool:
153
+ return self.variant is None
154
+
155
+
156
+ def parse_named(text: Any) -> tuple[str, float, str | None] | None:
157
+ """``(scale, level, variant or None)`` of a named color (``"pastel blue 0.3"``);
158
+ ``None`` when ``text`` is not one. A level outside 0–1 or an unknown variant word
159
+ before a scale name raises ``ValueError``."""
160
+ if not isinstance(text, str):
161
+ return None
162
+ m = _RE.match(text)
163
+ if m is None:
164
+ return None
165
+ name = m.group(2).lower()
166
+ name = _ALIASES.get(name, name)
167
+ if name not in SCALES:
168
+ return None
169
+ variant = m.group(1)
170
+ if variant is not None:
171
+ variant = variant.lower()
172
+ if variant not in VARIANTS:
173
+ raise ValueError(f"{text!r}: unknown color variant {m.group(1)!r} (variants: "
174
+ f"{', '.join(VARIANTS)})")
175
+ level = 0.5 if m.group(3) is None else float(m.group(3)) / (100 if m.group(4) else 1)
176
+ if not 0 <= level <= 1:
177
+ raise ValueError(f"{text!r}: the level of a named color is in 0–1, got {level:g}")
178
+ return name, level, variant
179
+
180
+
181
+ def scale_color(text: Any, variant: str = "base") -> str | None:
182
+ """``"#rrggbb"`` of a named color (``"name"``, ``"name level"``, level 0–1 or a
183
+ percentage, a variant word first or ``variant``); ``None`` when ``text`` is not a
184
+ named color. A level outside 0–1 or an unknown variant word raises ``ValueError``."""
185
+ p = parse_named(text)
186
+ if p is None:
187
+ return None
188
+ return _hex(p[0], p[1], p[2] or variant)
189
+
190
+
191
+ def variant_of(obj: Any) -> str:
192
+ """The ``color_variant`` in effect for ``obj``: its own setting, else its nearest
193
+ ancestor's that has the setting; ``"base"`` when none has it."""
194
+ o = obj
195
+ while o is not None:
196
+ settings = getattr(o, "SETTINGS", None)
197
+ if settings is not None and "color_variant" in settings:
198
+ return o.resolve("color_variant")
199
+ parent = getattr(o, "_parent", None)
200
+ o = parent() if parent is not None else None
201
+ return "base"
202
+
203
+
204
+ def _color(v: Any) -> Any:
205
+ """A color vec_io accepts, a named color (``"blue"``, ``"pastel blue 0.45"``) or a
206
+ reference to the plot's colors ``"C0"``, ``"C1"``, …"""
207
+ if isinstance(v, Fixed): # already checked
208
+ return v
209
+ if isinstance(v, str) and len(v) >= 2 and v[0] in "Cc" and v[1:].isascii() and v[1:].isdigit():
210
+ return "C" + v[1:]
211
+ named = parse_named(v)
212
+ if named is not None:
213
+ return Named(v.strip(), *named)
214
+ try:
215
+ rgba = to_rgba(v)
216
+ except (TypeError, ValueError):
217
+ rgba = None
218
+ if rgba is None and not (isinstance(v, str) and v.strip().lower() in ("none", "transparent")):
219
+ raise ValueError(f"invalid color {v!r}{hints.color(v)}")
220
+ return v
221
+
222
+
223
+ #: The validator of colors (``color(v)`` checks ``v``).
224
+ color = Color(_color)
225
+
226
+
227
+ def resolve_color(c: Any, cycle: tuple[Any, ...]) -> Any:
228
+ """A color with ``"C<n>"`` replaced by the plot's color ``n`` (of ``cycle``)."""
229
+ if isinstance(c, str) and len(c) >= 2 and c[0] == "C" and c[1:].isascii() and c[1:].isdigit():
230
+ return cycle[int(c[1:]) % len(cycle)]
231
+ if isinstance(c, str) and c.strip().lower() in ("none", "transparent"):
232
+ return None
233
+ return c
234
+
235
+
236
+ def with_alpha(c: Any, opacity: float) -> Any:
237
+ """``c`` with its alpha multiplied by ``opacity`` (as an RGBA tuple)."""
238
+ if c is None:
239
+ return None
240
+ if opacity >= 1:
241
+ return c
242
+ if isinstance(c, CMYK):
243
+ return c.with_alpha(c.alpha * opacity)
244
+ rgba = rgba_of(c)
245
+ if rgba is None:
246
+ return None
247
+ a = rgba[3] if len(rgba) > 3 else 1.0
248
+ return (rgba[0], rgba[1], rgba[2], a * opacity)
249
+
250
+
251
+ def desaturate(c: Any, k: float) -> Any:
252
+ """``c`` with its HLS saturation multiplied by ``k`` (as an RGBA tuple)."""
253
+ rgba = rgba_of(c)
254
+ if rgba is None:
255
+ return None
256
+ h, light, s = colorsys.rgb_to_hls(rgba[0], rgba[1], rgba[2])
257
+ r, g, b = colorsys.hls_to_rgb(h, light, s * k)
258
+ return (r, g, b, rgba[3] if len(rgba) > 3 else 1.0)
259
+
260
+
261
+ def luminance(c: Any) -> float:
262
+ """Luma of ``c`` (0–1): Rec. 709 weights of its sRGB components (not linearized),
263
+ composited over white."""
264
+ if c is None:
265
+ return 1.0
266
+ rgba = rgba_of(c)
267
+ if rgba is None:
268
+ return 1.0
269
+ r, g, b = (float(x) for x in rgba[:3])
270
+ a = float(rgba[3]) if len(rgba) > 3 else 1.0
271
+ lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
272
+ return a * lum + (1.0 - a) # over a white background
273
+
274
+
275
+ def ink_for(fill: Any) -> str:
276
+ """Black or white, whichever contrasts more with ``fill``."""
277
+ return "black" if luminance(fill) > 0.45 else "white"
278
+
279
+
280
+ def split_colors(text: str) -> list[str] | None:
281
+ """The colors of a comma-separated list (``"violet 0.4, sky, rgb(1 2 3)"``; commas
282
+ inside parentheses do not split), or ``None`` when ``text`` has no list comma."""
283
+ parts, depth, start = [], 0, 0
284
+ for i, ch in enumerate(text):
285
+ if ch == "(":
286
+ depth += 1
287
+ elif ch == ")":
288
+ depth -= 1
289
+ elif ch == "," and depth == 0:
290
+ parts.append(text[start:i])
291
+ start = i + 1
292
+ if not parts:
293
+ return None
294
+ parts.append(text[start:])
295
+ return [p.strip() for p in parts]