plot-io 0.12.0__py3-none-win_arm64.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-arm64.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,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
+ }