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
plot_io/occupancy.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""What the series cover inside the axes, on a coarse grid: used to place legends."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import kernels
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ["Occupancy", "thin", "extremes", "N", "MAX_VERTICES", "MAX_POINTS"]
|
|
10
|
+
|
|
11
|
+
#: Cells across each side of the axes.
|
|
12
|
+
N = 64
|
|
13
|
+
#: Polylines and curves are thinned to at most this many vertices before they are marked.
|
|
14
|
+
MAX_VERTICES = 4096
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#: Points beyond this many are sampled (every k-th) before they are marked.
|
|
18
|
+
MAX_POINTS = 50_000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _stride(n: int) -> int:
|
|
22
|
+
return max(1, -(-n // MAX_VERTICES))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def thin(*columns: Any, limit: int = MAX_POINTS) -> tuple[Any, ...]:
|
|
26
|
+
"""Every k-th item of each column, so that at most ``limit`` are left (data are thinned
|
|
27
|
+
before they are mapped to the page, which keeps legend placement fast)."""
|
|
28
|
+
n = min(len(c) for c in columns)
|
|
29
|
+
k = max(1, -(-n // limit))
|
|
30
|
+
if k == 1:
|
|
31
|
+
return columns
|
|
32
|
+
return tuple(c[:n:k] for c in columns)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extremes(a: Any, b: Any, limit: int = MAX_VERTICES) -> tuple[Any, Any]:
|
|
36
|
+
"""A polyline of more than ``4 · limit`` vertices thinned to strides of ``n / limit``
|
|
37
|
+
vertices, each kept by its first vertex and those of least and largest ``a`` and ``b``
|
|
38
|
+
(in order, so that narrow spikes stay); a stride with NaN keeps its first NaN (a
|
|
39
|
+
break) and the extremes of its other vertices."""
|
|
40
|
+
n = min(len(a), len(b))
|
|
41
|
+
k = -(-n // limit)
|
|
42
|
+
if k <= 4:
|
|
43
|
+
return a[:n], b[:n]
|
|
44
|
+
keep: list[int] = []
|
|
45
|
+
for i in range(0, n, k):
|
|
46
|
+
sa, sb = a[i:i + k], b[i:i + k]
|
|
47
|
+
ta, tb = sum(sa), sum(sb)
|
|
48
|
+
if ta == ta and tb == tb and abs(ta) != math.inf and abs(tb) != math.inf:
|
|
49
|
+
idx = {0, sa.index(min(sa)), sa.index(max(sa)), sb.index(min(sb)),
|
|
50
|
+
sb.index(max(sb))}
|
|
51
|
+
else:
|
|
52
|
+
good = [m for m in range(len(sa)) if math.isfinite(sa[m]) and math.isfinite(sb[m])]
|
|
53
|
+
idx = {m for m in range(len(sa)) if not (sa[m] == sa[m] and sb[m] == sb[m])}
|
|
54
|
+
idx = {min(idx)} if idx else set()
|
|
55
|
+
if good:
|
|
56
|
+
idx |= {good[0], min(good, key=sa.__getitem__), max(good, key=sa.__getitem__),
|
|
57
|
+
min(good, key=sb.__getitem__), max(good, key=sb.__getitem__)}
|
|
58
|
+
keep.extend(i + m for m in sorted(idx))
|
|
59
|
+
return [a[i] for i in keep], [b[i] for i in keep]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Occupancy:
|
|
63
|
+
"""An ``N`` × ``N`` grid of cells over the axes rectangle (page units); a cell is
|
|
64
|
+
occupied when some series draws in it. ``marks``: the same grid of the cells under
|
|
65
|
+
markers and solid boxes only (texts and guides inside the axes; a marker or a text
|
|
66
|
+
under a legend is hidden, a line or an area is not); ``discs`` and ``solids``: those
|
|
67
|
+
markers (page positions and radius) and boxes themselves, which :meth:`solid_in` tests
|
|
68
|
+
a box against exactly."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, rect: tuple[float, float, float, float]) -> None:
|
|
71
|
+
x0, y0, x1, y1 = rect
|
|
72
|
+
self.x0, self.y0 = x0, y0
|
|
73
|
+
self.cw = max(x1 - x0, 1e-9) / N
|
|
74
|
+
self.ch = max(y1 - y0, 1e-9) / N
|
|
75
|
+
self.cells = bytearray(N * N)
|
|
76
|
+
self.marks = bytearray(N * N)
|
|
77
|
+
self.discs: list[tuple[Any, Any, float]] = []
|
|
78
|
+
self.solids: list[tuple[float, float, float, float]] = []
|
|
79
|
+
self._xe = [x0 + i * self.cw for i in range(N + 1)]
|
|
80
|
+
self._ye = [y0 + j * self.ch for j in range(N + 1)]
|
|
81
|
+
|
|
82
|
+
# -- marking ---------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def _col(self, x: float) -> int:
|
|
85
|
+
return min(N - 1, max(0, int((x - self.x0) / self.cw)))
|
|
86
|
+
|
|
87
|
+
def _row(self, y: float) -> int:
|
|
88
|
+
return min(N - 1, max(0, int((y - self.y0) / self.ch)))
|
|
89
|
+
|
|
90
|
+
def _span(self, r: int, c0: int, c1: int, grid: bytearray | None = None) -> None:
|
|
91
|
+
if c1 >= c0:
|
|
92
|
+
(self.cells if grid is None else grid)[r * N + c0:r * N + c1 + 1] = \
|
|
93
|
+
b"\x01" * (c1 - c0 + 1)
|
|
94
|
+
|
|
95
|
+
def points(self, px: Any, py: Any, radius: float = 0.0, marker: bool = False) -> None:
|
|
96
|
+
"""Marks the cells holding points (counted in C; points outside are ignored), and
|
|
97
|
+
the cells within ``radius`` (px, a marker's half size) of those; ``marker``: in
|
|
98
|
+
``marks`` too."""
|
|
99
|
+
if not len(px):
|
|
100
|
+
return
|
|
101
|
+
if marker:
|
|
102
|
+
self.discs.append((px, py, radius))
|
|
103
|
+
_, _, counts = kernels.histogram2d(px, py, self._xe, self._ye)
|
|
104
|
+
kx = int(radius / self.cw + 0.5)
|
|
105
|
+
ky = int(radius / self.ch + 0.5)
|
|
106
|
+
grids = (self.cells, self.marks) if marker else (self.cells,)
|
|
107
|
+
for k, c in enumerate(counts):
|
|
108
|
+
if not c:
|
|
109
|
+
continue
|
|
110
|
+
if not kx and not ky:
|
|
111
|
+
for g in grids:
|
|
112
|
+
g[k] = 1
|
|
113
|
+
continue
|
|
114
|
+
r, col = divmod(k, N)
|
|
115
|
+
c0, c1 = max(0, col - kx), min(N - 1, col + kx)
|
|
116
|
+
for rr in range(max(0, r - ky), min(N - 1, r + ky) + 1):
|
|
117
|
+
for g in grids:
|
|
118
|
+
self._span(rr, c0, c1, g)
|
|
119
|
+
|
|
120
|
+
def lines(self, px: Any, py: Any) -> None:
|
|
121
|
+
"""Marks the cells a polyline passes through (NaN breaks it; a long one thinned by
|
|
122
|
+
:func:`extremes`); segments are sampled at half a cell, so long segments count
|
|
123
|
+
along their whole length (at most ``8 · MAX_VERTICES`` samples in all, spread by
|
|
124
|
+
length)."""
|
|
125
|
+
n = min(len(px), len(py))
|
|
126
|
+
if n == 0:
|
|
127
|
+
return
|
|
128
|
+
verts = list(zip(*extremes(px, py)))
|
|
129
|
+
step = 0.5 * min(self.cw, self.ch)
|
|
130
|
+
counts = []
|
|
131
|
+
for (x0, y0), (x1, y1) in zip(verts, verts[1:]):
|
|
132
|
+
d = max(abs(x1 - x0), abs(y1 - y0))
|
|
133
|
+
counts.append(int(d / step) if d == d else -1)
|
|
134
|
+
total = sum(c for c in counts if c > 0)
|
|
135
|
+
budget = 8 * MAX_VERTICES
|
|
136
|
+
scale = budget / total if total > budget else 1.0
|
|
137
|
+
xs, ys = [], []
|
|
138
|
+
for (x0, y0), (x1, y1), k in zip(verts, verts[1:], counts):
|
|
139
|
+
if k < 0:
|
|
140
|
+
continue
|
|
141
|
+
k = int(k * scale)
|
|
142
|
+
for j in range(k + 1):
|
|
143
|
+
t = j / (k + 1)
|
|
144
|
+
xs.append(x0 + (x1 - x0) * t)
|
|
145
|
+
ys.append(y0 + (y1 - y0) * t)
|
|
146
|
+
x, y = verts[-1]
|
|
147
|
+
if x == x and y == y:
|
|
148
|
+
xs.append(x)
|
|
149
|
+
ys.append(y)
|
|
150
|
+
self.points(xs, ys)
|
|
151
|
+
|
|
152
|
+
def rects(self, x0: Any, y0: Any, x1: Any, y1: Any, solid: bool = False) -> None:
|
|
153
|
+
"""Marks the cells under rectangles (any corner order; NaN rectangles skipped);
|
|
154
|
+
``solid``: texts and guides, in ``marks`` and ``solids`` too."""
|
|
155
|
+
X0, Y0 = self.x0, self.y0
|
|
156
|
+
X1, Y1 = X0 + N * self.cw, Y0 + N * self.ch
|
|
157
|
+
for a, b, c, d in zip(x0, y0, x1, y1):
|
|
158
|
+
if a != a or b != b or c != c or d != d:
|
|
159
|
+
continue
|
|
160
|
+
a, c = min(a, c), max(a, c)
|
|
161
|
+
b, d = min(b, d), max(b, d)
|
|
162
|
+
if c < X0 or a > X1 or d < Y0 or b > Y1:
|
|
163
|
+
continue
|
|
164
|
+
c0, c1 = self._col(a), self._col(c)
|
|
165
|
+
for r in range(self._row(b), self._row(d) + 1):
|
|
166
|
+
self._span(r, c0, c1)
|
|
167
|
+
if solid:
|
|
168
|
+
self._span(r, c0, c1, self.marks)
|
|
169
|
+
if solid:
|
|
170
|
+
self.solids.append((a, b, c, d))
|
|
171
|
+
|
|
172
|
+
def band(self, t: Any, lo: Any, hi: Any, horizontal: bool = False) -> None:
|
|
173
|
+
"""Marks the region between two curves sharing the coordinate ``t`` (page x, or page
|
|
174
|
+
y when ``horizontal``): per grid column (row), from the least to the largest value
|
|
175
|
+
of the samples in it, with samples added at half a cell along ``t``."""
|
|
176
|
+
n = min(len(t), len(lo), len(hi))
|
|
177
|
+
if n == 0:
|
|
178
|
+
return
|
|
179
|
+
size = self.ch if horizontal else self.cw
|
|
180
|
+
index = self._row if horizontal else self._col
|
|
181
|
+
mins = [math.inf] * N
|
|
182
|
+
maxs = [-math.inf] * N
|
|
183
|
+
s = _stride(n)
|
|
184
|
+
prev: tuple[float, float, float] | None = None
|
|
185
|
+
T0 = self.y0 if horizontal else self.x0
|
|
186
|
+
T1 = T0 + N * size
|
|
187
|
+
|
|
188
|
+
def put(tv: float, a: float, b: float) -> None:
|
|
189
|
+
if not T0 <= tv <= T1: # outside the view
|
|
190
|
+
return
|
|
191
|
+
k = index(tv)
|
|
192
|
+
lo_, hi_ = (a, b) if a <= b else (b, a)
|
|
193
|
+
if lo_ < mins[k]:
|
|
194
|
+
mins[k] = lo_
|
|
195
|
+
if hi_ > maxs[k]:
|
|
196
|
+
maxs[k] = hi_
|
|
197
|
+
|
|
198
|
+
for i in range(0, n, s):
|
|
199
|
+
tv, a, b = t[i], lo[i], hi[i]
|
|
200
|
+
if tv != tv or a != a or b != b:
|
|
201
|
+
prev = None
|
|
202
|
+
continue
|
|
203
|
+
if prev is not None:
|
|
204
|
+
k = int(abs(tv - prev[0]) / (0.5 * size))
|
|
205
|
+
for j in range(1, k + 1):
|
|
206
|
+
f = j / (k + 1)
|
|
207
|
+
put(prev[0] + (tv - prev[0]) * f, prev[1] + (a - prev[1]) * f,
|
|
208
|
+
prev[2] + (b - prev[2]) * f)
|
|
209
|
+
put(tv, a, b)
|
|
210
|
+
prev = (tv, a, b)
|
|
211
|
+
for k in range(N):
|
|
212
|
+
if mins[k] > maxs[k]:
|
|
213
|
+
continue
|
|
214
|
+
centre = T0 + (k + 0.5) * size
|
|
215
|
+
if not T0 <= centre <= T1:
|
|
216
|
+
continue
|
|
217
|
+
if horizontal:
|
|
218
|
+
self._span(k, self._col(mins[k]), self._col(maxs[k]))
|
|
219
|
+
else:
|
|
220
|
+
for r in range(self._row(mins[k]), self._row(maxs[k]) + 1):
|
|
221
|
+
self.cells[r * N + k] = 1
|
|
222
|
+
|
|
223
|
+
# -- queries ---------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
def _sums(self) -> list[int]:
|
|
226
|
+
"""Summed-area table: ``s[(r) * (N + 1) + c]`` = occupied cells above and left."""
|
|
227
|
+
s = [0] * ((N + 1) * (N + 1))
|
|
228
|
+
cells = self.cells
|
|
229
|
+
for r in range(N):
|
|
230
|
+
run = 0
|
|
231
|
+
base, prev = (r + 1) * (N + 1), r * (N + 1)
|
|
232
|
+
for c in range(N):
|
|
233
|
+
run += cells[r * N + c]
|
|
234
|
+
s[base + c + 1] = s[prev + c + 1] + run
|
|
235
|
+
return s
|
|
236
|
+
|
|
237
|
+
def covered(self, x: float, y: float, w: float, h: float, sums: list[int] | None = None,
|
|
238
|
+
markers: bool = False) -> float:
|
|
239
|
+
"""Fraction of the cells whose centres lie in the box ``(x, y, w, h)`` that are
|
|
240
|
+
occupied (``markers``: under markers; 0 when the box holds no cell centre).
|
|
241
|
+
``sums`` (from :meth:`_sums`, of all cells) makes repeated queries constant time."""
|
|
242
|
+
c0 = max(0, math.ceil((x - self.x0) / self.cw - 0.5))
|
|
243
|
+
c1 = min(N - 1, math.floor((x + w - self.x0) / self.cw - 0.5))
|
|
244
|
+
r0 = max(0, math.ceil((y - self.y0) / self.ch - 0.5))
|
|
245
|
+
r1 = min(N - 1, math.floor((y + h - self.y0) / self.ch - 0.5))
|
|
246
|
+
if c1 < c0 or r1 < r0:
|
|
247
|
+
return 0.0
|
|
248
|
+
if sums is None or markers:
|
|
249
|
+
cells = self.marks if markers else self.cells
|
|
250
|
+
hit = sum(cells[r * N + c0:r * N + c1 + 1].count(1) for r in range(r0, r1 + 1))
|
|
251
|
+
else:
|
|
252
|
+
M = N + 1
|
|
253
|
+
hit = (sums[(r1 + 1) * M + c1 + 1] - sums[r0 * M + c1 + 1]
|
|
254
|
+
- sums[(r1 + 1) * M + c0] + sums[r0 * M + c0])
|
|
255
|
+
return hit / ((c1 - c0 + 1) * (r1 - r0 + 1))
|
|
256
|
+
|
|
257
|
+
def solid_in(self, x: float, y: float, w: float, h: float) -> int:
|
|
258
|
+
"""The markers whose disc meets the box ``(x, y, w, h)`` (by the box grown by
|
|
259
|
+
each marker's radius) and the solid boxes it meets."""
|
|
260
|
+
n = sum(1 for a, b, c, d in self.solids if a < x + w and x < c and b < y + h and y < d)
|
|
261
|
+
for px, py, r in self.discs:
|
|
262
|
+
r = max(r, 0.5)
|
|
263
|
+
n += int(kernels.histogram2d(px, py, (x - r, x + w + r),
|
|
264
|
+
(y - r, y + h + r))[2][0])
|
|
265
|
+
return n
|
|
266
|
+
|
|
267
|
+
def free_boxes(self, w: float, h: float, pad: float) -> list[tuple[float, float]]:
|
|
268
|
+
"""Top-left corners, one cell apart, of the boxes ``w`` × ``h`` inside the axes
|
|
269
|
+
(``pad`` from their edges) that cover no occupied cell."""
|
|
270
|
+
sums = self._sums()
|
|
271
|
+
x_lo, x_hi = self.x0 + pad, self.x0 + N * self.cw - pad - w
|
|
272
|
+
y_lo, y_hi = self.y0 + pad, self.y0 + N * self.ch - pad - h
|
|
273
|
+
if x_hi < x_lo or y_hi < y_lo:
|
|
274
|
+
return []
|
|
275
|
+
nx = int((x_hi - x_lo) / self.cw)
|
|
276
|
+
ny = int((y_hi - y_lo) / self.ch)
|
|
277
|
+
xs = [x_lo + i * self.cw for i in range(nx + 1)] + [x_hi]
|
|
278
|
+
ys = [y_lo + j * self.ch for j in range(ny + 1)] + [y_hi]
|
|
279
|
+
# the cells whose centres a box at each x (y) holds, as :meth:`covered` takes them
|
|
280
|
+
cols = [(max(0, math.ceil((x - self.x0) / self.cw - 0.5)),
|
|
281
|
+
min(N - 1, math.floor((x + w - self.x0) / self.cw - 0.5))) for x in xs]
|
|
282
|
+
rows = [(max(0, math.ceil((y - self.y0) / self.ch - 0.5)),
|
|
283
|
+
min(N - 1, math.floor((y + h - self.y0) / self.ch - 0.5))) for y in ys]
|
|
284
|
+
M = N + 1
|
|
285
|
+
out = []
|
|
286
|
+
for y, (r0, r1) in zip(ys, rows):
|
|
287
|
+
if r1 < r0:
|
|
288
|
+
out += [(x, y) for x in xs]
|
|
289
|
+
continue
|
|
290
|
+
top, bottom = r0 * M, (r1 + 1) * M
|
|
291
|
+
for x, (c0, c1) in zip(xs, cols):
|
|
292
|
+
if c1 < c0 or (sums[bottom + c1 + 1] - sums[top + c1 + 1]
|
|
293
|
+
- sums[bottom + c0] + sums[top + c0]) == 0:
|
|
294
|
+
out.append((x, y))
|
|
295
|
+
return out
|