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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Contours (stat ``contour``): filled bands (``Polygons``), iso-lines (``Path``) and level
|
|
2
|
+
labels on the lines (``Text``)."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from vec_io.arrays import doubles
|
|
9
|
+
|
|
10
|
+
from .._methods import ContourMixin
|
|
11
|
+
from .. import groups
|
|
12
|
+
from .. import kernels
|
|
13
|
+
from ..color.values import with_alpha
|
|
14
|
+
from ..layers import stats
|
|
15
|
+
from ..layers.base import Series, common
|
|
16
|
+
from ..layers.core import Call, Layer, Table
|
|
17
|
+
from ..layers.grammar import entry
|
|
18
|
+
from ..layers.paint import Paint, role
|
|
19
|
+
from ..labels import drop, quad
|
|
20
|
+
from ..layers.geoms import GEOMS
|
|
21
|
+
from ..lines import dash_of
|
|
22
|
+
from ..mapped import color_axis_of, unit_of
|
|
23
|
+
from ..numbers import NumberStyle, check_format, format_numbers
|
|
24
|
+
from ..settings import Setting, boolean, either, literal, number, one_of, table
|
|
25
|
+
from ..text import font_of, measure
|
|
26
|
+
from ..ticks import nice_step
|
|
27
|
+
from ..transform import settings_both as coord_settings
|
|
28
|
+
from ._util import columns_meta, given_colors, map_or_color, placed_text
|
|
29
|
+
from .heatmap import Coords
|
|
30
|
+
|
|
31
|
+
__all__ = ["Contour"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _levels(v: Any) -> Any:
|
|
35
|
+
if isinstance(v, int) and not isinstance(v, bool):
|
|
36
|
+
return number(lo=1, integer=True)(v)
|
|
37
|
+
vals = sorted(float(x) for x in v)
|
|
38
|
+
if len(vals) < 1:
|
|
39
|
+
raise ValueError("give at least one level")
|
|
40
|
+
return vals
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_COLOR_MAP_DOC = ("The colors of the levels: a map (a qualitative one's colors taken in "
|
|
44
|
+
"turn by the lines), one color for every line, `\"auto\"` or a kind "
|
|
45
|
+
"(as a series' `color_map`).")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Contour(ContourMixin, Series):
|
|
49
|
+
"""Contour lines and/or filled bands of ``values`` (rows first) at nodes ``x``/``y``
|
|
50
|
+
(centres; edges are reduced to their midpoints).
|
|
51
|
+
|
|
52
|
+
``lines`` draws iso-lines and ``fill`` filled bands. ``levels``: a number of nice values
|
|
53
|
+
over the data, or the values. Lines take the map's colors: of a qualitative
|
|
54
|
+
``color_map`` in turn, else at their level; ``color_map`` as one color draws them in
|
|
55
|
+
that color; ``negative_style`` dashes
|
|
56
|
+
negative levels drawn in one color; ``value_labels`` writes the levels on the lines.
|
|
57
|
+
Bands are colored by their middle value.
|
|
58
|
+
|
|
59
|
+
Example::
|
|
60
|
+
|
|
61
|
+
import math
|
|
62
|
+
grid = [[math.sin(i / 5) * math.cos(j / 6) for j in range(40)] for i in range(30)]
|
|
63
|
+
p = plt.Plot(width=240, height=160)
|
|
64
|
+
p.contour(grid, value_labels=True)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
SETTINGS = table(
|
|
68
|
+
**coord_settings(),
|
|
69
|
+
levels=Setting(_levels, doc="A number of levels (nice values over the data) or "
|
|
70
|
+
"the level values."),
|
|
71
|
+
**groups.FillStyle.embed(checks={"fill": lambda v: groups.fill_value(v, colors=False)},
|
|
72
|
+
docs={"fill": "Fill the bands between levels: `True` or "
|
|
73
|
+
"`False`.",
|
|
74
|
+
"fill_opacity": "Opacity of the bands, times "
|
|
75
|
+
"`opacity`."}),
|
|
76
|
+
lines=Setting(boolean, doc="Draw the iso-lines."),
|
|
77
|
+
**{**common("color_map"),
|
|
78
|
+
"color_map": Setting(map_or_color, doc=_COLOR_MAP_DOC)},
|
|
79
|
+
**groups.Stroke.embed(only=("line_width", "line_style"),
|
|
80
|
+
inherited=("line_width",),
|
|
81
|
+
docs={"line_width": "Line width; default 0.6 × the plot's.",
|
|
82
|
+
"line_style": "Line style."}),
|
|
83
|
+
negative_style=Setting(either(literal(None), one_of("solid", "dashed", "dotted",
|
|
84
|
+
"dashdot")),
|
|
85
|
+
doc="Line style of negative levels drawn with a single color "
|
|
86
|
+
"(matplotlib: dashed)."),
|
|
87
|
+
opacity=Setting(number(0, 1), doc="Opacity."),
|
|
88
|
+
value_labels=Setting(boolean, doc="Write the level values on the lines (a label over an "
|
|
89
|
+
"earlier one is left out)."),
|
|
90
|
+
value_label_format=Setting(check_format, doc="Number format of the value labels."),
|
|
91
|
+
**groups.TextStyle.embed("value_label_",
|
|
92
|
+
only=("font_size",),
|
|
93
|
+
docs={"font_size": "Label font size."}),
|
|
94
|
+
**common("name", "z", "rasterize", "simplify", "font_size", "font_family", "text_color"),
|
|
95
|
+
**groups.NumberFormat.embed(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
DATA = {"values": "raw", "x": "raw", "y": "raw"}
|
|
99
|
+
|
|
100
|
+
def __init__(self, values: Any, x: Any = None, y: Any = None, plot: Any = None,
|
|
101
|
+
**settings: Any) -> None:
|
|
102
|
+
values, x, y = self.cols["values"], self.cols["x"], self.cols["y"]
|
|
103
|
+
g = kernels.grid(values)
|
|
104
|
+
if g is not None:
|
|
105
|
+
self.h, self.w, k, self.flat = g
|
|
106
|
+
if k != 1:
|
|
107
|
+
raise ValueError("contour values must be a 2-D grid of numbers")
|
|
108
|
+
else:
|
|
109
|
+
rows = [list(r) for r in values]
|
|
110
|
+
self.h, self.w = len(rows), len(rows[0]) if rows else 0
|
|
111
|
+
if any(len(r) != self.w for r in rows):
|
|
112
|
+
raise ValueError("contour rows must have the same length")
|
|
113
|
+
self.flat = doubles([v for r in rows for v in r])
|
|
114
|
+
if self.h < 2 or self.w < 2:
|
|
115
|
+
raise ValueError("contours need a grid of at least 2 × 2 values")
|
|
116
|
+
self.xn = self._nodes(x, self.w, "x")
|
|
117
|
+
self.yn = self._nodes(y, self.h, "y")
|
|
118
|
+
super().__init__(plot=plot, **settings)
|
|
119
|
+
|
|
120
|
+
def _bar_source(self) -> Any:
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _nodes(given: Any, n: int, what: str) -> array.array:
|
|
125
|
+
if given is None:
|
|
126
|
+
return array.array("d", (float(i) for i in range(n)))
|
|
127
|
+
c = Coords(given, n, what)
|
|
128
|
+
if c.kind == "category":
|
|
129
|
+
raise ValueError("contour coordinates must be numbers")
|
|
130
|
+
v = c.values
|
|
131
|
+
if c.kind == "edges":
|
|
132
|
+
return array.array("d", ((a + b) / 2 for a, b in zip(v, v[1:])))
|
|
133
|
+
return array.array("d", v)
|
|
134
|
+
|
|
135
|
+
def _scales(self) -> tuple[Any, Any]:
|
|
136
|
+
return stats.axis_scale(self.plot.x_axis), stats.axis_scale(self.plot.y_axis)
|
|
137
|
+
|
|
138
|
+
# -- levels and map ---------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def level_values(self) -> list[float]:
|
|
141
|
+
lv = self.resolve("levels")
|
|
142
|
+
if isinstance(lv, list):
|
|
143
|
+
return lv
|
|
144
|
+
if isinstance(lv, bool) or not isinstance(lv, (int, float)):
|
|
145
|
+
raise ValueError(f"{self.kind} setting 'levels': expected a number of levels or "
|
|
146
|
+
f"the level values, got {lv!r}")
|
|
147
|
+
n, lo, hi = kernels.extent(self.flat, None)
|
|
148
|
+
if not n:
|
|
149
|
+
return [0.0]
|
|
150
|
+
if hi == lo:
|
|
151
|
+
return [lo]
|
|
152
|
+
step = nice_step((hi - lo) / lv)
|
|
153
|
+
start = math.floor(lo / step + 1e-9) * step
|
|
154
|
+
end = math.ceil(hi / step - 1e-9) * step
|
|
155
|
+
k = int(round((end - start) / step))
|
|
156
|
+
return [round(start + i * step, 12) for i in range(k + 1)]
|
|
157
|
+
|
|
158
|
+
def _filled(self) -> bool:
|
|
159
|
+
return self.resolve("fill") is not False
|
|
160
|
+
|
|
161
|
+
def _fill_opacity(self) -> float:
|
|
162
|
+
return groups.fill_parts(self)[1]
|
|
163
|
+
|
|
164
|
+
def _grid_data(self) -> tuple[Any, int, int, Any, Any]:
|
|
165
|
+
"""``(flat, w, h, fx, fy)``: the grid and its nodes after the axis scales."""
|
|
166
|
+
sx, sy = self._scales()
|
|
167
|
+
fx = array.array("d", (sx.f(v) for v in self.xn))
|
|
168
|
+
fy = array.array("d", (sy.f(v) for v in self.yn))
|
|
169
|
+
return self.flat, self.w, self.h, fx, fy
|
|
170
|
+
|
|
171
|
+
def _bands(self, lv: list[float]) -> list[tuple[float, float, float]]:
|
|
172
|
+
out = []
|
|
173
|
+
for lo, hi in zip(lv, lv[1:]):
|
|
174
|
+
top = hi if hi != lv[-1] else math.nextafter(hi, math.inf)
|
|
175
|
+
out.append((lo, hi, top))
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def stats(self) -> dict[str, Any]:
|
|
180
|
+
"""``{"levels", "lines": {level: [(x, y, closed)]}, "bands": [(lo, hi, (xs, ys,
|
|
181
|
+
ring_starts))]}``, coordinates after the axis scale functions."""
|
|
182
|
+
sx, sy = self._scales()
|
|
183
|
+
return self.cached(("stats", stats.scale_key(sx), stats.scale_key(sy)), self._compute,
|
|
184
|
+
stats.READS["contour"], kind="stats")
|
|
185
|
+
|
|
186
|
+
def _compute(self) -> dict[str, Any]:
|
|
187
|
+
flat, w, h, fx, fy = self._grid_data()
|
|
188
|
+
lv = self.level_values()
|
|
189
|
+
bands = self._bands(lv) if self._filled() else []
|
|
190
|
+
return stats.contour(flat, w, h, fx, fy, lv, bool(self.resolve("lines")), bands)
|
|
191
|
+
|
|
192
|
+
def _given(self) -> Any:
|
|
193
|
+
"""The lines' colors in place of the map's at their levels: one color, a qualitative
|
|
194
|
+
map's colors (taken in turn), or ``None``."""
|
|
195
|
+
return given_colors(self, len(self.level_values()), ("qualitative",))
|
|
196
|
+
|
|
197
|
+
def _level_color(self, ctx: Any, i: int, level: float) -> Any:
|
|
198
|
+
given = self._given()
|
|
199
|
+
if given is not None:
|
|
200
|
+
return ctx.color(given[i % len(given)] if isinstance(given, list) else given)
|
|
201
|
+
axis = color_axis_of(self)
|
|
202
|
+
return axis.color_map()(unit_of(axis)(level))
|
|
203
|
+
|
|
204
|
+
# -- the preset --------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def layers(self) -> Call:
|
|
207
|
+
call = Call(self)
|
|
208
|
+
st = self.stats
|
|
209
|
+
opacity = self.resolve("opacity")
|
|
210
|
+
key = id(self)
|
|
211
|
+
if st["bands"]:
|
|
212
|
+
band_opacity = opacity * self._fill_opacity()
|
|
213
|
+
|
|
214
|
+
call.layers.append(Layer(GEOMS["polygons"],
|
|
215
|
+
Table(ring_sets=[r for _, _, r in st["bands"]]),
|
|
216
|
+
paint=Paint(self, "contour.bands",
|
|
217
|
+
opacity=band_opacity),
|
|
218
|
+
stat="contour", key=False))
|
|
219
|
+
lw = self.resolve("line_width")
|
|
220
|
+
style = self.resolve("line_style")
|
|
221
|
+
neg = self.resolve("negative_style")
|
|
222
|
+
given = self._given()
|
|
223
|
+
single = given is not None and not isinstance(given, list)
|
|
224
|
+
drawn = [] # (level index, level, polylines)
|
|
225
|
+
for i, level in enumerate(st["levels"]):
|
|
226
|
+
polylines = st["lines"].get(level) or []
|
|
227
|
+
if not polylines or not lw:
|
|
228
|
+
continue
|
|
229
|
+
drawn.append((i, level, polylines))
|
|
230
|
+
ls = neg if (neg is not None and single and level < 0) else style
|
|
231
|
+
|
|
232
|
+
call.layers.append(Layer(GEOMS["path"], Table(x=[p[0] for p in polylines],
|
|
233
|
+
y=[p[1] for p in polylines]),
|
|
234
|
+
paint=Paint(self, "contour.line", i=i, level=level,
|
|
235
|
+
dash=dash_of(ls, lw), opacity=opacity),
|
|
236
|
+
stat="contour", key=False, lists=True,
|
|
237
|
+
f_space=True))
|
|
238
|
+
if self.resolve("value_labels") and drawn:
|
|
239
|
+
size = self.resolve("value_label_font_size")
|
|
240
|
+
call.layers.append(placed_text(self, key, lambda ctx: self._label_items(ctx, drawn),
|
|
241
|
+
lambda ctx: font_of(self, size),
|
|
242
|
+
background=lambda ctx: self._background(ctx)))
|
|
243
|
+
self._occupancy(call)
|
|
244
|
+
filled = self._filled()
|
|
245
|
+
glyph = Layer(GEOMS["polygons" if filled else "path"], Table(),
|
|
246
|
+
paint=Paint(self, "contour.key"), glyph="fill")
|
|
247
|
+
name = self.resolve("name")
|
|
248
|
+
if name is not None:
|
|
249
|
+
call.entries.append(entry(self, name, [glyph], stroke="_key_color()",
|
|
250
|
+
fill="_key_color()" if filled else None))
|
|
251
|
+
lv = self.level_values()
|
|
252
|
+
call.color_count = self._color_count()
|
|
253
|
+
if self._mapped_source():
|
|
254
|
+
call.color_sources = [(color_axis_of(self), array.array("d", lv))]
|
|
255
|
+
self._extent(call)
|
|
256
|
+
call.exact = {"x", "y"}
|
|
257
|
+
columns_meta(call, self._roles())
|
|
258
|
+
return call
|
|
259
|
+
|
|
260
|
+
def _color_count(self) -> int:
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
def _key_color(self, ctx: Any) -> Any:
|
|
264
|
+
"""The legend's color: the middle level's."""
|
|
265
|
+
lv = self.level_values()
|
|
266
|
+
return self._level_color(ctx, len(lv) // 2, lv[len(lv) // 2])
|
|
267
|
+
|
|
268
|
+
def _mapped_source(self) -> bool:
|
|
269
|
+
return not (self._given() is not None and not self._filled())
|
|
270
|
+
|
|
271
|
+
def _roles(self) -> dict[str, str]:
|
|
272
|
+
return {"values": "x"}
|
|
273
|
+
|
|
274
|
+
def _occupancy(self, call: Call) -> None:
|
|
275
|
+
for la in call.layers:
|
|
276
|
+
la.occupy = "all" if not la.overlay else False
|
|
277
|
+
|
|
278
|
+
def _extent(self, call: Call) -> None:
|
|
279
|
+
call.extra = {"x": self.xn, "y": self.yn}
|
|
280
|
+
|
|
281
|
+
def _background(self, ctx: Any) -> Any:
|
|
282
|
+
fig = self.plot.figure
|
|
283
|
+
return next((c for c in (ctx.color(self.plot.resolve("background")),
|
|
284
|
+
ctx.color(fig.resolve("background")), "white")
|
|
285
|
+
if c is not None))
|
|
286
|
+
|
|
287
|
+
def _label_items(self, ctx: Any, drawn: list[Any]) -> list[Any]:
|
|
288
|
+
canvas = ctx.canvas
|
|
289
|
+
xm, ym = ctx.xmap, ctx.ymap
|
|
290
|
+
levels = [lv for _, lv, _ in drawn]
|
|
291
|
+
texts = format_numbers(levels, self.resolve("value_label_format"), NumberStyle.of(self))
|
|
292
|
+
size = self.resolve("value_label_font_size")
|
|
293
|
+
opacity = self.resolve("opacity")
|
|
294
|
+
x0, y0, x1, y1 = ctx.rect
|
|
295
|
+
out, quads = [], []
|
|
296
|
+
for (i, level, polylines), text in zip(drawn, texts):
|
|
297
|
+
c = with_alpha(self._level_color(ctx, i, level), opacity)
|
|
298
|
+
font = font_of(self, size, color=c)
|
|
299
|
+
w, a, d = measure(canvas, text, font)
|
|
300
|
+
for p in polylines:
|
|
301
|
+
xs, ys = kernels.scaled(p[0], xm.a, xm.b), kernels.scaled(p[1], ym.a, ym.b)
|
|
302
|
+
n = len(xs)
|
|
303
|
+
if n < 3:
|
|
304
|
+
continue
|
|
305
|
+
seg = [math.hypot(xs[k + 1] - xs[k], ys[k + 1] - ys[k]) for k in range(n - 1)]
|
|
306
|
+
total = sum(seg)
|
|
307
|
+
if total < 3 * w:
|
|
308
|
+
continue
|
|
309
|
+
# the label goes at the middle of the line
|
|
310
|
+
half, acc, k = total / 2, 0.0, 0
|
|
311
|
+
while k < n - 2 and acc + seg[k] < half:
|
|
312
|
+
acc += seg[k]
|
|
313
|
+
k += 1
|
|
314
|
+
t = (half - acc) / seg[k] if seg[k] else 0.0
|
|
315
|
+
cx = xs[k] + t * (xs[k + 1] - xs[k])
|
|
316
|
+
cy = ys[k] + t * (ys[k + 1] - ys[k])
|
|
317
|
+
if not (x0 <= cx <= x1 and y0 <= cy <= y1):
|
|
318
|
+
continue
|
|
319
|
+
ang = math.degrees(math.atan2(-(ys[k + 1] - ys[k]), xs[k + 1] - xs[k]))
|
|
320
|
+
if ang > 90:
|
|
321
|
+
ang -= 180
|
|
322
|
+
elif ang < -90:
|
|
323
|
+
ang += 180
|
|
324
|
+
box = None if self._filled() else (w, a + d, 0.25 * size)
|
|
325
|
+
out.append((text, cx, cy, c, "center", "center", ang, box, None))
|
|
326
|
+
quads.append(quad(canvas, text, font, cx, cy, "center", "center", ang))
|
|
327
|
+
return [t for t, k in zip(out, drop(quads)) if k]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@role("contour.bands")
|
|
331
|
+
def _bands_paint(s: Contour, ctx: Any, glyph: bool, opacity: float = 1.0) -> dict[str, Any]:
|
|
332
|
+
"""Filled bands in the colors of their middle values."""
|
|
333
|
+
axis = color_axis_of(s)
|
|
334
|
+
cmap, t = axis.color_map(), unit_of(axis)
|
|
335
|
+
return {"fill": [with_alpha(cmap(t((lo + hi) / 2)), opacity)
|
|
336
|
+
for lo, hi, _ in s.stats["bands"]],
|
|
337
|
+
"simplify": s.resolve("simplify")}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@role("contour.line")
|
|
341
|
+
def _line_paint(s: Contour, ctx: Any, glyph: bool, i: int = 0, level: float = 0.0,
|
|
342
|
+
dash: Any = None, opacity: Any = None) -> dict[str, Any]:
|
|
343
|
+
"""The lines of level ``i``."""
|
|
344
|
+
lw = s.resolve("line_width")
|
|
345
|
+
return {"color": with_alpha(s._level_color(ctx, i, level), opacity), "width": lw,
|
|
346
|
+
"dash": dash, "simplify": s.resolve("simplify")}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@role("contour.key")
|
|
350
|
+
def _key_paint(s: Contour, ctx: Any, glyph: bool) -> dict[str, Any]:
|
|
351
|
+
"""The legend symbol: a swatch of the middle level's color (filled), or its line."""
|
|
352
|
+
c = s._key_color(ctx)
|
|
353
|
+
return {"fill": c, "color": c, "width": s.resolve("line_width")}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Line and fill interpolation and missing values (vec_io draws every mode in C:
|
|
2
|
+
``interpolation=``, ``missing=`` on ``draw_lines`` and ``draw_area``)."""
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..settings import Setting, one_of
|
|
7
|
+
|
|
8
|
+
__all__ = ["INTERPOLATIONS", "curve_settings"]
|
|
9
|
+
|
|
10
|
+
INTERPOLATIONS = ("linear", "step-pre", "step-post", "step-mid", "monotone", "spline")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _interpolation(v: Any) -> str:
|
|
14
|
+
s = str(getattr(v, "value", v)).strip().lower().replace("_", "-")
|
|
15
|
+
if s not in INTERPOLATIONS:
|
|
16
|
+
raise ValueError(f"expected one of {', '.join(map(repr, INTERPOLATIONS))}, got {v!r}")
|
|
17
|
+
return s
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def curve_settings() -> dict[str, Setting]:
|
|
21
|
+
return {
|
|
22
|
+
"interpolation": Setting(
|
|
23
|
+
_interpolation,
|
|
24
|
+
doc="How points are joined: `linear`, `step-pre`, `step-post`, `step-mid`, "
|
|
25
|
+
"`monotone` (a curve that never overshoots) or `spline` (centripetal "
|
|
26
|
+
"Catmull–Rom)."),
|
|
27
|
+
"missing": Setting(one_of("break", "skip"),
|
|
28
|
+
doc="Missing values: `break` the line there, or `skip` them and "
|
|
29
|
+
"connect their neighbours."),
|
|
30
|
+
}
|