plot-io 0.1.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.
- plot_io/__init__.py +66 -0
- plot_io/_backend.py +114 -0
- plot_io/axis.py +574 -0
- plot_io/colorbar.py +342 -0
- plot_io/decorations.py +496 -0
- plot_io/defaults.py +766 -0
- plot_io/density.py +97 -0
- plot_io/enums.py +63 -0
- plot_io/facets.py +184 -0
- plot_io/figure.py +331 -0
- plot_io/labels.py +63 -0
- plot_io/legend.py +498 -0
- plot_io/limits.py +92 -0
- plot_io/maps/__init__.py +365 -0
- plot_io/maps/data.py +333 -0
- plot_io/markers.py +117 -0
- plot_io/numbers.py +289 -0
- plot_io/occupancy.py +219 -0
- plot_io/palettes.py +142 -0
- plot_io/plot.py +435 -0
- plot_io/render.py +1122 -0
- plot_io/scales.py +93 -0
- plot_io/series/__init__.py +498 -0
- plot_io/series/area.py +181 -0
- plot_io/series/bar.py +294 -0
- plot_io/series/box.py +401 -0
- plot_io/series/catdata.py +23 -0
- plot_io/series/catpoints.py +182 -0
- plot_io/series/contour.py +294 -0
- plot_io/series/curves.py +128 -0
- plot_io/series/ecdf.py +143 -0
- plot_io/series/errorbar.py +111 -0
- plot_io/series/errors.py +100 -0
- plot_io/series/fill.py +168 -0
- plot_io/series/heatmap.py +304 -0
- plot_io/series/hexbin.py +216 -0
- plot_io/series/hist.py +379 -0
- plot_io/series/hist2d.py +163 -0
- plot_io/series/kde.py +228 -0
- plot_io/series/kde2d.py +153 -0
- plot_io/series/line.py +173 -0
- plot_io/series/pie.py +304 -0
- plot_io/series/points.py +341 -0
- plot_io/series/regression.py +250 -0
- plot_io/series/scatter.py +103 -0
- plot_io/series/stem.py +112 -0
- plot_io/series/violin.py +321 -0
- plot_io/settings.py +281 -0
- plot_io/shapes.py +57 -0
- plot_io/sharing.py +160 -0
- plot_io/slots.py +52 -0
- plot_io/stacking.py +63 -0
- plot_io/stats.py +140 -0
- plot_io/style.py +71 -0
- plot_io/styles/__init__.py +234 -0
- plot_io/styles/library.py +732 -0
- plot_io/tables.py +141 -0
- plot_io/text.py +109 -0
- plot_io/ticks.py +284 -0
- plot_io/times.py +199 -0
- plot_io/values.py +161 -0
- plot_io-0.1.0.dist-info/METADATA +102 -0
- plot_io-0.1.0.dist-info/RECORD +65 -0
- plot_io-0.1.0.dist-info/WHEEL +4 -0
- plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
plot_io/axis.py
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"""Axes: settings (:class:`Axis`) and their state while a figure is drawn
|
|
2
|
+
(:class:`AxisState`: scale, limits, ticks, mapping to the page)."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import _backend
|
|
9
|
+
from .limits import check_limits, needs_quantiles, resolve_limits
|
|
10
|
+
from .scales import AXIS_SCALES, Scale, get_scale
|
|
11
|
+
from .settings import (Setting, Settings, boolean, color, either, literal, number, one_of,
|
|
12
|
+
pair, table, text)
|
|
13
|
+
from .text import Font, box
|
|
14
|
+
from .numbers import NumberStyle, check_format, format_axis, number_settings
|
|
15
|
+
from .ticks import Tick, minor_linear, numeric_ticks
|
|
16
|
+
|
|
17
|
+
__all__ = ["Axis", "AxisState", "Mapper"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _ticks_value(v: Any) -> Any:
|
|
21
|
+
if v is False:
|
|
22
|
+
return False
|
|
23
|
+
if isinstance(v, (list, tuple)):
|
|
24
|
+
return [float(x) if not isinstance(x, str) else x for x in v]
|
|
25
|
+
raise ValueError(f"expected False or a list of values, got {v!r}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _minor_value(v: Any) -> Any:
|
|
29
|
+
if isinstance(v, bool):
|
|
30
|
+
return v
|
|
31
|
+
if isinstance(v, (list, tuple)):
|
|
32
|
+
return [float(x) for x in v]
|
|
33
|
+
raise ValueError(f"expected True, False or a list of values, got {v!r}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _rotation(v: Any) -> Any:
|
|
37
|
+
if v == "auto":
|
|
38
|
+
return "auto"
|
|
39
|
+
return number(-180, 180)(v)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _dash(v: Any) -> Any:
|
|
43
|
+
if isinstance(v, (list, tuple)):
|
|
44
|
+
return [number(lo=0)(x) for x in v]
|
|
45
|
+
return one_of("solid", "dashed", "dotted", "dashdot")(v)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Axis(Settings):
|
|
49
|
+
"""An axis of a plot (``plot.x_axis``, ``plot.y_axis``) or of a colorbar."""
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def nominal_scale(self) -> str:
|
|
53
|
+
"""``scale``, or ``"linear"`` while unset (before it is detected from the data)."""
|
|
54
|
+
s = self.get("scale")
|
|
55
|
+
return "linear" if s is None else s
|
|
56
|
+
|
|
57
|
+
SETTINGS = table(
|
|
58
|
+
label=Setting(text, doc="Axis title: a string or rich content."),
|
|
59
|
+
label_size=Setting(number(positive=True),
|
|
60
|
+
doc="Font size of the label (pt); default: the base font size.", length=True),
|
|
61
|
+
scale=Setting(one_of(*AXIS_SCALES),
|
|
62
|
+
doc="`linear`, `log`, `log2`, `log10`, `sqrt`, `exp`, `symlog`, "
|
|
63
|
+
"`logit`, `time` or `category`; default: `category` for "
|
|
64
|
+
"categorical series, `time` for dates, else `linear`."),
|
|
65
|
+
linthresh=Setting(number(positive=True),
|
|
66
|
+
doc="Half-width of the linear region of a `symlog` axis."),
|
|
67
|
+
tz=Setting(lambda v: v,
|
|
68
|
+
doc="Time zone of a time axis (a `tzinfo`); default: the zone of the "
|
|
69
|
+
"first aware date, else naive (UTC)."),
|
|
70
|
+
limits=Setting(check_limits,
|
|
71
|
+
doc="`\"auto\"`, `\"lo/hi\"` (`min`, `max`, numbers, `q:F`) or `(lo, hi)`."),
|
|
72
|
+
padding=Setting(either(literal("auto"), number(lo=0)),
|
|
73
|
+
doc="Space added to automatic limits, as a fraction of the range; "
|
|
74
|
+
"`\"auto\"`: `auto_padding`, or 0 on axes of series drawn to "
|
|
75
|
+
"exact limits (box value axes, heatmaps, …) and in plots "
|
|
76
|
+
"without axes."),
|
|
77
|
+
auto_padding=Setting(number(lo=0),
|
|
78
|
+
doc="The fraction `padding=\"auto\"` adds on each side."),
|
|
79
|
+
invert=Setting(boolean, doc="Reverse the axis."),
|
|
80
|
+
ticks=Setting(_ticks_value, doc="`False` (no ticks) or a list of tick values."),
|
|
81
|
+
tick_labels=Setting(lambda v: [text(x) for x in v], doc="Labels of `ticks`."),
|
|
82
|
+
minor_ticks=Setting(_minor_value,
|
|
83
|
+
doc="`True`, `False` or a list of values; default: on for log10."),
|
|
84
|
+
tick_format=Setting(check_format,
|
|
85
|
+
doc="Number format: `auto`, `percent`, `sci`, `eng`, `si`, `int`, "
|
|
86
|
+
"a format string or a function."),
|
|
87
|
+
tick_rotation=Setting(_rotation,
|
|
88
|
+
doc="Label rotation in degrees, or `\"auto\"` (45° when "
|
|
89
|
+
"category labels overlap)."),
|
|
90
|
+
max_ticks=Setting(either(literal("auto"), number(lo=1, integer=True)),
|
|
91
|
+
doc="Most major ticks; `\"auto\"`: one per `tick_spacing` of "
|
|
92
|
+
"axis length, at least 2, at most `auto_max_ticks`."),
|
|
93
|
+
tick_spacing=Setting(pair(number(positive=True)),
|
|
94
|
+
doc="Space per automatic tick, in tick label font sizes, on "
|
|
95
|
+
"horizontal and vertical axes."),
|
|
96
|
+
time_tick_spacing=Setting(pair(number(positive=True)),
|
|
97
|
+
doc="`tick_spacing` of time axes."),
|
|
98
|
+
auto_max_ticks=Setting(pair(number(lo=2, integer=True)),
|
|
99
|
+
doc="Most automatic ticks on numeric and time axes."),
|
|
100
|
+
sci_limits=Setting(pair(number(integer=True)),
|
|
101
|
+
doc="Exponents beyond which labels become mantissas × 10ᵏ."),
|
|
102
|
+
tick_length=Setting(number(lo=0), doc="Tick length (pt).", length=True),
|
|
103
|
+
minor_tick_length=Setting(number(lo=0),
|
|
104
|
+
doc="Minor tick length (pt); default 0.6 × `tick_length`.", length=True),
|
|
105
|
+
tick_direction=Setting(one_of("out", "in", "both"), doc="`out`, `in` or `both`."),
|
|
106
|
+
tick_width=Setting(number(lo=0), doc="Tick line width (pt).", length=True),
|
|
107
|
+
tick_size=Setting(number(positive=True),
|
|
108
|
+
doc="Font size of tick labels (pt).", length=True),
|
|
109
|
+
tick_pad=Setting(number(lo=0), doc="Gap between ticks and labels (pt).", length=True),
|
|
110
|
+
label_pad=Setting(number(lo=0), doc="Gap between tick labels and the label (pt).", length=True),
|
|
111
|
+
color=Setting(color,
|
|
112
|
+
doc="Color of the axis line, ticks and labels (the default of "
|
|
113
|
+
"`line_color`, `tick_color`, `tick_label_color`, `label_color`)."),
|
|
114
|
+
line_color=Setting(color, doc="Axis line color."),
|
|
115
|
+
tick_color=Setting(color, doc="Tick color."),
|
|
116
|
+
tick_label_color=Setting(color, doc="Tick label color."),
|
|
117
|
+
label_color=Setting(color, doc="Axis title color."),
|
|
118
|
+
label_weight=Setting(one_of("normal", "bold"), doc="Axis title weight."),
|
|
119
|
+
label_style=Setting(one_of("normal", "italic"), doc="Axis title style."),
|
|
120
|
+
side=Setting(one_of("left", "right", "bottom", "top"),
|
|
121
|
+
doc="Where the ticks, labels and axis line go: `bottom` or `top` (x "
|
|
122
|
+
"axis), `left` or `right` (y axis)."),
|
|
123
|
+
tick_label_weight=Setting(one_of("normal", "bold"),
|
|
124
|
+
doc="Tick label weight."),
|
|
125
|
+
mirror=Setting(boolean, doc="Ticks also on the opposite side of the axes."),
|
|
126
|
+
spine_offset=Setting(number(lo=0),
|
|
127
|
+
doc="Distance the axis line (with its ticks and labels) is "
|
|
128
|
+
"moved away from the axes (pt).", length=True),
|
|
129
|
+
spine_trim=Setting(boolean,
|
|
130
|
+
doc="Axis line from the first to the last major tick only."),
|
|
131
|
+
include_zero=Setting(either(boolean, literal("auto")),
|
|
132
|
+
doc="Extend automatic limits to 0 (linear and sqrt scales), "
|
|
133
|
+
"before padding and `nice`: `True`, or `\"auto\"` where a "
|
|
134
|
+
"series on the axis asks for it (Vega-Lite's rule: scatter "
|
|
135
|
+
"axes, the value axis of lines, areas and boxes)."),
|
|
136
|
+
zero_line=Setting(boolean, doc="A line across the plot at 0."),
|
|
137
|
+
zero_line_color=Setting(color,
|
|
138
|
+
doc="Zero line color; default `line_color`."),
|
|
139
|
+
zero_line_width=Setting(number(lo=0),
|
|
140
|
+
doc="Zero line width (pt); default `line_width`.",
|
|
141
|
+
length=True),
|
|
142
|
+
nice=Setting(boolean,
|
|
143
|
+
doc="Extend automatic limits to the enclosing major ticks (linear and "
|
|
144
|
+
"log scales)."),
|
|
145
|
+
line_width=Setting(number(lo=0), doc="Axis line width (pt).", length=True),
|
|
146
|
+
grid=Setting(boolean, doc="Major grid lines across the plot."),
|
|
147
|
+
grid_color=Setting(color, doc="Grid line color."),
|
|
148
|
+
grid_width=Setting(number(lo=0), doc="Grid line width (pt).", length=True),
|
|
149
|
+
grid_style=Setting(_dash, doc="Grid line style or dash list."),
|
|
150
|
+
minor_grid=Setting(boolean,
|
|
151
|
+
doc="Minor grid lines: at the minor ticks, or halfway between "
|
|
152
|
+
"major ticks on linear axes without minor ticks."),
|
|
153
|
+
minor_grid_color=Setting(color,
|
|
154
|
+
doc="Minor grid color; default `grid_color`."),
|
|
155
|
+
minor_grid_width=Setting(number(lo=0),
|
|
156
|
+
doc="Minor grid width (pt); default half `grid_width`.",
|
|
157
|
+
length=True),
|
|
158
|
+
minor_grid_style=Setting(_dash,
|
|
159
|
+
doc="Minor grid style; default `grid_style`."),
|
|
160
|
+
visible=Setting(boolean, doc="Draw the axis (ticks, labels)."),
|
|
161
|
+
font_size=Setting(number(positive=True), inherited=True, doc="Base font size (pt).", length=True),
|
|
162
|
+
font_family=Setting(str, inherited=True, doc="Font family."),
|
|
163
|
+
text_color=Setting(color, inherited=True, doc="Text color."),
|
|
164
|
+
**number_settings(),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def __init__(self, owner: Any, role: str) -> None:
|
|
168
|
+
self._owner = owner
|
|
169
|
+
self.role = role
|
|
170
|
+
super().__init__()
|
|
171
|
+
|
|
172
|
+
def _parent(self) -> Settings | None:
|
|
173
|
+
return self._owner
|
|
174
|
+
|
|
175
|
+
def _theme_keys(self, name: str) -> tuple[str, ...]:
|
|
176
|
+
from .colorbar import Colorbar
|
|
177
|
+
if isinstance(self._owner, Colorbar):
|
|
178
|
+
if name in Colorbar.SETTINGS:
|
|
179
|
+
return (f"Axis.{name}",)
|
|
180
|
+
return (f"Colorbar.{name}", f"Axis.{name}")
|
|
181
|
+
return (f"{self.role.upper()}Axis.{name}", f"Axis.{name}")
|
|
182
|
+
|
|
183
|
+
def __call__(self, label: Any = None, **settings: Any) -> "Axis":
|
|
184
|
+
if label is not None:
|
|
185
|
+
settings["label"] = label
|
|
186
|
+
return self.set(**settings)
|
|
187
|
+
|
|
188
|
+
def __repr__(self) -> str:
|
|
189
|
+
return f"Axis({self.role})"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Mapper:
|
|
193
|
+
"""Data → page coordinate on one axis: ``page = a·f(v) + b``."""
|
|
194
|
+
|
|
195
|
+
__slots__ = ("function", "a", "b", "forward", "options")
|
|
196
|
+
|
|
197
|
+
def __init__(self, scale: Scale | None, flo: float, fhi: float, p0: float, p1: float) -> None:
|
|
198
|
+
self.function = scale.function if scale is not None else None
|
|
199
|
+
self.options = scale.options if scale is not None else {}
|
|
200
|
+
self.forward = scale.f if scale is not None else (lambda v: v)
|
|
201
|
+
span = fhi - flo
|
|
202
|
+
self.a = (p1 - p0) / span if span else 0.0
|
|
203
|
+
self.b = p0 - self.a * flo
|
|
204
|
+
|
|
205
|
+
def __call__(self, v: float) -> float:
|
|
206
|
+
return self.a * self.forward(v) + self.b
|
|
207
|
+
|
|
208
|
+
def many(self, values: Any) -> Any:
|
|
209
|
+
return _backend.scaled(values, self.a, self.b, self.function, **self.options)
|
|
210
|
+
|
|
211
|
+
def position(self, f: float) -> float:
|
|
212
|
+
"""Page coordinate of a value already through the scale function."""
|
|
213
|
+
return self.a * f + self.b
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class AxisState:
|
|
217
|
+
"""An axis while drawing: resolved scale, limits, categories and ticks."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, axis: Axis) -> None:
|
|
220
|
+
self.axis = axis
|
|
221
|
+
self.scale: Scale | None = None # None: category
|
|
222
|
+
self.categories: list[Any] = []
|
|
223
|
+
self.lo = 0.0 # data limits (category: index range)
|
|
224
|
+
self.hi = 1.0
|
|
225
|
+
self.flo = 0.0 # limits after the scale function
|
|
226
|
+
self.fhi = 1.0
|
|
227
|
+
self.data_lo = math.nan # union of the series' extents
|
|
228
|
+
self.data_hi = math.nan
|
|
229
|
+
self.ticks: list[Tick] = []
|
|
230
|
+
self.factor = 0 # ×10^factor written at the axis end
|
|
231
|
+
self.rotation = 0.0
|
|
232
|
+
self.length = 0.0
|
|
233
|
+
self.auto_invert = False # inverted by a series (heatmap rows)
|
|
234
|
+
self.bare = False # category axis of heatmaps only: labels only
|
|
235
|
+
self._auto = (False, False) # automatic low / high limit (for `nice`)
|
|
236
|
+
self._zero_pad = False # a series wants exact limits (heatmaps)
|
|
237
|
+
self._base = (0.0, 1.0, 0.0, 1.0) # lo, hi, flo, fhi before `nice`
|
|
238
|
+
self.shared: "AxisState | None" = None # the group's leader (shared axes)
|
|
239
|
+
self.hide_labels = False # an inner plot of a shared group
|
|
240
|
+
self.auto_label: Any = None # the column name of the series' data
|
|
241
|
+
self.tz: Any = None # time zone of a time axis
|
|
242
|
+
self.offset: str | None = None # larger date unit common to the time ticks
|
|
243
|
+
|
|
244
|
+
def on_far_side(self) -> bool:
|
|
245
|
+
"""Whether the axis is drawn on the top (x) or right (y) side of the axes."""
|
|
246
|
+
return self.axis.get("side") in ("top", "right")
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def is_category(self) -> bool:
|
|
250
|
+
return self.scale is None
|
|
251
|
+
|
|
252
|
+
def f(self, v: float) -> float:
|
|
253
|
+
return self.scale.f(v) if self.scale is not None else v
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def inverted(self) -> bool:
|
|
257
|
+
if self.shared is not None:
|
|
258
|
+
return self.shared.inverted
|
|
259
|
+
a = self.axis
|
|
260
|
+
return a.get("invert") if a.is_set("invert") else self.auto_invert
|
|
261
|
+
|
|
262
|
+
def tick_out(self) -> tuple[float, float]:
|
|
263
|
+
"""Tick extent outside and inside the axes."""
|
|
264
|
+
a = self.axis
|
|
265
|
+
if self.bare and not a.is_set("tick_length"):
|
|
266
|
+
return 0.0, 0.0
|
|
267
|
+
n = a.get("tick_length")
|
|
268
|
+
d = a.get("tick_direction")
|
|
269
|
+
return (n if d in ("out", "both") else 0.0), (n if d in ("in", "both") else 0.0)
|
|
270
|
+
|
|
271
|
+
def mapper(self, p0: float, p1: float) -> Mapper:
|
|
272
|
+
inv = self.inverted
|
|
273
|
+
if inv:
|
|
274
|
+
p0, p1 = p1, p0
|
|
275
|
+
return Mapper(self.scale, self.flo, self.fhi, p0, p1)
|
|
276
|
+
|
|
277
|
+
# -- limits ------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
def resolve(self, series: Sequence[Any], padded: bool) -> None:
|
|
280
|
+
"""Scale, categories and limits from the series drawn on this axis."""
|
|
281
|
+
role = self.axis.role
|
|
282
|
+
scale_name = self.axis.get("scale")
|
|
283
|
+
cats: list[Any] = []
|
|
284
|
+
categorical = False
|
|
285
|
+
for s in series:
|
|
286
|
+
c = s.categories(role)
|
|
287
|
+
if c is not None:
|
|
288
|
+
categorical = True
|
|
289
|
+
for k in c:
|
|
290
|
+
if k not in cats:
|
|
291
|
+
cats.append(k)
|
|
292
|
+
on_axis = [s for s in series if s.has_axis(role)]
|
|
293
|
+
self.auto_invert = any(s.inverts(role) for s in on_axis)
|
|
294
|
+
times = [s for s in on_axis if getattr(s, "time_role", lambda r: None)(role)]
|
|
295
|
+
if scale_name is None:
|
|
296
|
+
scale_name = "category" if categorical else ("time" if times else "linear")
|
|
297
|
+
self.tz = self.axis.get("tz")
|
|
298
|
+
if self.tz is None and times:
|
|
299
|
+
self.tz = times[0].time_role(role)[1]
|
|
300
|
+
from .series.heatmap import Heatmap
|
|
301
|
+
self.bare = (scale_name == "category" and bool(on_axis)
|
|
302
|
+
and all(isinstance(s, Heatmap) for s in on_axis))
|
|
303
|
+
if scale_name == "category":
|
|
304
|
+
if any(s.categories(role) is None and s.has_axis(role) for s in series):
|
|
305
|
+
raise ValueError(f"{role} axis mixes categorical and numeric series")
|
|
306
|
+
self.scale = None
|
|
307
|
+
self.categories = cats
|
|
308
|
+
n = max(len(cats), 1)
|
|
309
|
+
self.lo, self.hi = -0.5, n - 0.5
|
|
310
|
+
spec = self.axis.get("limits")
|
|
311
|
+
if spec != "auto":
|
|
312
|
+
lo, hi, _, _ = resolve_limits(spec, self.lo, self.hi)
|
|
313
|
+
self.lo, self.hi = float(lo), float(hi)
|
|
314
|
+
self.flo, self.fhi = self.lo, self.hi
|
|
315
|
+
return
|
|
316
|
+
if categorical:
|
|
317
|
+
raise ValueError(f"{role} axis has categorical series but scale {scale_name!r}")
|
|
318
|
+
scale = self.scale = get_scale(scale_name, self.axis.get("linthresh"))
|
|
319
|
+
count, lo, hi = 0, math.inf, -math.inf
|
|
320
|
+
zero_pad = False
|
|
321
|
+
for s in series:
|
|
322
|
+
if not s.has_axis(role):
|
|
323
|
+
continue
|
|
324
|
+
n, a, b = s.extent(role, scale)
|
|
325
|
+
if n:
|
|
326
|
+
count += n
|
|
327
|
+
lo, hi = min(lo, a), max(hi, b)
|
|
328
|
+
zero_pad = zero_pad or s.zero_padding(role)
|
|
329
|
+
if not count:
|
|
330
|
+
lo, hi = (1.0, 10.0) if scale.name.startswith("log") else (0.0, 1.0)
|
|
331
|
+
self.data_lo, self.data_hi = lo, hi
|
|
332
|
+
zero = self.axis.get("include_zero")
|
|
333
|
+
if zero == "auto":
|
|
334
|
+
zero = any(s.zero_default(role) for s in on_axis)
|
|
335
|
+
if zero and scale.name in ("linear", "sqrt"):
|
|
336
|
+
lo, hi = min(lo, 0.0), max(hi, 0.0)
|
|
337
|
+
spec = self.axis.get("limits")
|
|
338
|
+
qs = needs_quantiles(spec)
|
|
339
|
+
qmap: dict[float, float] = {}
|
|
340
|
+
if qs:
|
|
341
|
+
from array import array
|
|
342
|
+
vals = array("d")
|
|
343
|
+
for s in series:
|
|
344
|
+
if s.has_axis(role):
|
|
345
|
+
v = s.values(role)
|
|
346
|
+
if v is not None:
|
|
347
|
+
vals.extend(_backend.doubles(v))
|
|
348
|
+
if scale.function is not None:
|
|
349
|
+
vals = array("d", [v for v in vals if scale.contains(v)])
|
|
350
|
+
for q, r in zip(qs, _backend.quantiles(vals, qs)):
|
|
351
|
+
qmap[q] = r
|
|
352
|
+
lo2, hi2, lo_auto, hi_auto = resolve_limits(spec, lo, hi, lambda q: qmap[q])
|
|
353
|
+
flo, fhi = scale.f(lo2), scale.f(hi2)
|
|
354
|
+
if not (math.isfinite(flo) and math.isfinite(fhi)):
|
|
355
|
+
raise ValueError(f"{role} axis limits {lo2!r}/{hi2!r} are outside the "
|
|
356
|
+
f"{scale.name} scale's domain")
|
|
357
|
+
if flo > fhi:
|
|
358
|
+
flo, fhi = fhi, flo
|
|
359
|
+
lo_auto, hi_auto = hi_auto, lo_auto
|
|
360
|
+
if fhi == flo:
|
|
361
|
+
flo, fhi = flo - 0.5, fhi + 0.5
|
|
362
|
+
if scale.name == "sqrt" and flo < 0:
|
|
363
|
+
flo, fhi = 0.0, fhi + 0.5
|
|
364
|
+
else:
|
|
365
|
+
pad = self.axis.get("padding")
|
|
366
|
+
if pad == "auto":
|
|
367
|
+
pad = self.axis.get("auto_padding") if padded and not zero_pad else 0.0
|
|
368
|
+
d = (fhi - flo) * pad
|
|
369
|
+
if lo_auto:
|
|
370
|
+
flo -= d
|
|
371
|
+
if scale.name == "sqrt" and flo < 0:
|
|
372
|
+
flo = 0.0
|
|
373
|
+
if hi_auto:
|
|
374
|
+
fhi += d
|
|
375
|
+
if not math.isfinite(scale.inverse(flo)):
|
|
376
|
+
flo = scale.f(lo2) if scale.f(lo2) < fhi else flo + d
|
|
377
|
+
if not math.isfinite(scale.inverse(fhi)):
|
|
378
|
+
fhi = scale.f(hi2)
|
|
379
|
+
self.flo, self.fhi = flo, fhi
|
|
380
|
+
self.lo, self.hi = scale.inverse(flo), scale.inverse(fhi)
|
|
381
|
+
self._auto = (lo_auto, hi_auto)
|
|
382
|
+
self._zero_pad = zero_pad
|
|
383
|
+
self._base = (self.lo, self.hi, self.flo, self.fhi)
|
|
384
|
+
|
|
385
|
+
def copy_limits(self, other: "AxisState") -> None:
|
|
386
|
+
"""Take the scale, categories and limits of ``other`` (a shared group's leader)."""
|
|
387
|
+
for k in ("scale", "categories", "lo", "hi", "flo", "fhi", "data_lo", "data_hi",
|
|
388
|
+
"auto_invert", "bare", "_auto", "_zero_pad", "_base"):
|
|
389
|
+
setattr(self, k, getattr(other, k))
|
|
390
|
+
self.categories = list(other.categories)
|
|
391
|
+
|
|
392
|
+
# -- ticks -------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def tick_font(self) -> Font:
|
|
395
|
+
a = self.axis
|
|
396
|
+
return Font(a.get("tick_size"), a.get("font_family"), a.get("tick_label_weight"),
|
|
397
|
+
color=a.get("tick_label_color"))
|
|
398
|
+
|
|
399
|
+
def label_font(self) -> Font:
|
|
400
|
+
a = self.axis
|
|
401
|
+
return Font(a.get("label_size"), a.get("font_family"), a.get("label_weight"),
|
|
402
|
+
a.get("label_style"), color=a.get("label_color"))
|
|
403
|
+
|
|
404
|
+
def compute_ticks(self, canvas: Any, length: float, horizontal: bool) -> None:
|
|
405
|
+
"""Ticks for an axis ``length`` points long (``horizontal``: labels side by side);
|
|
406
|
+
with ``nice``, automatic limits are then extended to the enclosing major ticks."""
|
|
407
|
+
lead = self.shared
|
|
408
|
+
if lead is not None:
|
|
409
|
+
self.copy_limits(lead)
|
|
410
|
+
if lead.length and abs(lead.length - length) <= 0.5:
|
|
411
|
+
self.lo, self.hi, self.flo, self.fhi = lead.lo, lead.hi, lead.flo, lead.fhi
|
|
412
|
+
self.ticks, self.factor = list(lead.ticks), lead.factor
|
|
413
|
+
self.rotation, self.length = lead.rotation, length
|
|
414
|
+
return
|
|
415
|
+
nice = (self.scale is not None and self.axis.get("nice") and any(self._auto)
|
|
416
|
+
and not self._zero_pad
|
|
417
|
+
and (self.scale.name == "linear" or self.scale.name.startswith("log")))
|
|
418
|
+
if nice:
|
|
419
|
+
self.lo, self.hi, self.flo, self.fhi = self._base
|
|
420
|
+
self._compute_ticks(canvas, length, horizontal)
|
|
421
|
+
if not nice:
|
|
422
|
+
return
|
|
423
|
+
assert self.scale is not None
|
|
424
|
+
majors = sorted(self.major_positions())
|
|
425
|
+
if self.scale.name == "linear":
|
|
426
|
+
if len(majors) < 2:
|
|
427
|
+
return
|
|
428
|
+
step = majors[1] - majors[0]
|
|
429
|
+
else:
|
|
430
|
+
step = 1.0
|
|
431
|
+
flo, fhi = self.flo, self.fhi
|
|
432
|
+
eps = step * 1e-9
|
|
433
|
+
if self._auto[0]:
|
|
434
|
+
flo = math.floor((flo + eps) / step) * step
|
|
435
|
+
if self._auto[1]:
|
|
436
|
+
fhi = math.ceil((fhi - eps) / step) * step
|
|
437
|
+
if (flo, fhi) != (self.flo, self.fhi) and math.isfinite(self.scale.inverse(flo)) \
|
|
438
|
+
and math.isfinite(self.scale.inverse(fhi)):
|
|
439
|
+
self.flo, self.fhi = flo, fhi
|
|
440
|
+
self.lo, self.hi = self.scale.inverse(flo), self.scale.inverse(fhi)
|
|
441
|
+
self._compute_ticks(canvas, length, horizontal)
|
|
442
|
+
|
|
443
|
+
def minor_grid_positions(self) -> list[float]:
|
|
444
|
+
"""Values of the minor grid lines: the minor ticks, else (linear axes) halfway
|
|
445
|
+
between major ticks."""
|
|
446
|
+
minors = [t.value for t in self.ticks if not t.major]
|
|
447
|
+
if minors or self.scale is None or self.scale.name != "linear":
|
|
448
|
+
return minors
|
|
449
|
+
majors = sorted(t.value for t in self.ticks if t.major)
|
|
450
|
+
if len(majors) < 2:
|
|
451
|
+
return []
|
|
452
|
+
step = majors[1] - majors[0]
|
|
453
|
+
lo, hi = min(self.lo, self.hi), max(self.lo, self.hi)
|
|
454
|
+
out = [m + step / 2 for m in [majors[0] - step] + majors]
|
|
455
|
+
return [v for v in out if lo <= v <= hi]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _max_ticks(self, length: float, horizontal: bool, font_size: float,
|
|
459
|
+
time: bool = False) -> int:
|
|
460
|
+
"""``max_ticks``, or the automatic count for an axis of ``length`` pt."""
|
|
461
|
+
a = self.axis
|
|
462
|
+
most = a.get("max_ticks")
|
|
463
|
+
if most != "auto":
|
|
464
|
+
return int(most)
|
|
465
|
+
spacing = a.get("time_tick_spacing" if time else "tick_spacing")[0 if horizontal else 1]
|
|
466
|
+
cap = a.get("auto_max_ticks")[1 if time else 0]
|
|
467
|
+
return min(cap, max(2, int(length / (spacing * font_size)) + 1))
|
|
468
|
+
def _compute_ticks(self, canvas: Any, length: float, horizontal: bool) -> None:
|
|
469
|
+
a = self.axis
|
|
470
|
+
self.length = length
|
|
471
|
+
self.factor = 0
|
|
472
|
+
self.rotation = 0.0
|
|
473
|
+
given = a.get("ticks")
|
|
474
|
+
font = self.tick_font()
|
|
475
|
+
if given is False:
|
|
476
|
+
self.ticks = []
|
|
477
|
+
return
|
|
478
|
+
if self.is_category:
|
|
479
|
+
self._category_ticks(canvas, length, horizontal, font)
|
|
480
|
+
return
|
|
481
|
+
assert self.scale is not None
|
|
482
|
+
self.offset = None
|
|
483
|
+
if self.scale.name == "time" and given is None:
|
|
484
|
+
from .times import time_ticks
|
|
485
|
+
most = self._max_ticks(length, horizontal, font.size, time=True)
|
|
486
|
+
fmt = a.get("tick_format")
|
|
487
|
+
pairs, self.offset = time_ticks(self.lo, self.hi, most, self.tz,
|
|
488
|
+
fmt if fmt != "auto" else "auto")
|
|
489
|
+
self.ticks = [Tick(v, lab) for v, lab in pairs]
|
|
490
|
+
rot = a.get("tick_rotation")
|
|
491
|
+
self.rotation = 0.0 if rot == "auto" else float(rot)
|
|
492
|
+
return
|
|
493
|
+
fmt = a.get("tick_format")
|
|
494
|
+
style = NumberStyle.of(a)
|
|
495
|
+
if given is not None:
|
|
496
|
+
labels = a.get("tick_labels")
|
|
497
|
+
values = [float(v) for v in given]
|
|
498
|
+
if labels is None:
|
|
499
|
+
labels, self.factor = format_axis(values, fmt, style, a.get("sci_limits"))
|
|
500
|
+
self.ticks = [Tick(v, s) for v, s in zip(values, labels)]
|
|
501
|
+
else:
|
|
502
|
+
fs = font.size
|
|
503
|
+
max_ticks = self._max_ticks(length, horizontal, fs)
|
|
504
|
+
|
|
505
|
+
def fits(values: list[float], step: float) -> bool:
|
|
506
|
+
if not horizontal or len(values) < 2:
|
|
507
|
+
return True
|
|
508
|
+
labels, _ = format_axis(values, fmt, style, a.get("sci_limits"))
|
|
509
|
+
widest = max(box(canvas, s, font)[0] for s in labels)
|
|
510
|
+
span = self.fhi - self.flo
|
|
511
|
+
if not span:
|
|
512
|
+
return True
|
|
513
|
+
gap = length * abs(self.f(values[1]) - self.f(values[0])) / span
|
|
514
|
+
return widest + 0.6 * fs <= gap
|
|
515
|
+
|
|
516
|
+
minor = a.get("minor_ticks")
|
|
517
|
+
self.ticks, self.factor = numeric_ticks(
|
|
518
|
+
self.scale, self.lo, self.hi, max_ticks, fmt, style, a.get("sci_limits"),
|
|
519
|
+
minor=minor is not False, length=length,
|
|
520
|
+
fits=fits if self.scale.name == "linear" else None)
|
|
521
|
+
if minor is True and self.scale.name == "linear":
|
|
522
|
+
majors = [t.value for t in self.ticks if t.major]
|
|
523
|
+
if len(majors) >= 2:
|
|
524
|
+
step = majors[1] - majors[0]
|
|
525
|
+
have = set(majors)
|
|
526
|
+
self.ticks += [Tick(v, None, False) for v in minor_linear(self.lo, self.hi, step)
|
|
527
|
+
if not any(abs(v - m) < step * 1e-6 for m in have)]
|
|
528
|
+
minors = a.get("minor_ticks")
|
|
529
|
+
if isinstance(minors, list):
|
|
530
|
+
self.ticks += [Tick(v, None, False) for v in minors]
|
|
531
|
+
lo, hi = min(self.flo, self.fhi), max(self.flo, self.fhi)
|
|
532
|
+
eps = (hi - lo) * 1e-9
|
|
533
|
+
self.ticks = [t for t in self.ticks
|
|
534
|
+
if lo - eps <= self.f(t.value) <= hi + eps]
|
|
535
|
+
rot = a.get("tick_rotation")
|
|
536
|
+
self.rotation = 0.0 if rot == "auto" else float(rot)
|
|
537
|
+
|
|
538
|
+
def _category_ticks(self, canvas: Any, length: float, horizontal: bool, font: Font) -> None:
|
|
539
|
+
a = self.axis
|
|
540
|
+
cats = self.categories
|
|
541
|
+
labels = a.get("tick_labels") or [str(c) if not isinstance(c, (list, str)) else c
|
|
542
|
+
for c in cats]
|
|
543
|
+
fmt = a.get("tick_format")
|
|
544
|
+
if fmt != "auto" and not a.get("tick_labels"):
|
|
545
|
+
if all(isinstance(c, (int, float)) and not isinstance(c, bool) for c in cats):
|
|
546
|
+
labels = format_axis([float(c) for c in cats], fmt, NumberStyle.of(a), None)[0]
|
|
547
|
+
elif callable(fmt):
|
|
548
|
+
labels = [fmt(c) for c in cats]
|
|
549
|
+
elif fmt not in ("percent", "sci", "eng", "si", "int"):
|
|
550
|
+
labels = [fmt.format(c) for c in cats]
|
|
551
|
+
ticks = [Tick(float(i), lab) for i, lab in enumerate(labels)]
|
|
552
|
+
span = abs(self.fhi - self.flo) or 1.0
|
|
553
|
+
step = length / span # points per category
|
|
554
|
+
rot = a.get("tick_rotation")
|
|
555
|
+
fs = font.size
|
|
556
|
+
if not ticks:
|
|
557
|
+
self.ticks = []
|
|
558
|
+
return
|
|
559
|
+
if horizontal:
|
|
560
|
+
widest = max(box(canvas, lab, font)[0] for lab in labels)
|
|
561
|
+
if rot == "auto":
|
|
562
|
+
rot = 45.0 if widest + 0.5 * fs > step else 0.0
|
|
563
|
+
need = (widest + 0.5 * fs) if rot == 0 else 1.6 * fs
|
|
564
|
+
else:
|
|
565
|
+
rot = 0.0 if rot == "auto" else rot
|
|
566
|
+
need = 1.3 * fs
|
|
567
|
+
self.rotation = float(rot)
|
|
568
|
+
every = max(1, math.ceil(need / step)) if step > 0 else 1
|
|
569
|
+
if every > 1:
|
|
570
|
+
ticks = [t for i, t in enumerate(ticks) if i % every == 0]
|
|
571
|
+
self.ticks = ticks
|
|
572
|
+
|
|
573
|
+
def major_positions(self) -> list[float]:
|
|
574
|
+
return [self.f(t.value) for t in self.ticks if t.major]
|