From 4b983443c40004a91919634efb5e47bd4f607c1e Mon Sep 17 00:00:00 2001 From: Hanjin Liu Date: Wed, 7 Feb 2024 20:58:57 +0900 Subject: [PATCH] fix autoscaling, update doc --- docs/_scripts/_screenshots.py | 13 +- docs/canvas/grid.md | 44 +++-- docs/canvas/namespaces.md | 12 +- docs/layers/face_layers.md | 48 +++--- docs/layers/layer_groups.md | 177 +++++++++++++++++++-- docs/layers/lines.md | 35 ++++ docs/layers/markers.md | 8 +- whitecanvas/__init__.py | 2 +- whitecanvas/canvas/_base.py | 46 ++++-- whitecanvas/layers/_primitive/errorbars.py | 19 ++- whitecanvas/layers/_primitive/line.py | 82 ++++++---- whitecanvas/layers/_primitive/markers.py | 20 ++- whitecanvas/layers/group/labeled.py | 17 +- whitecanvas/types/_enums.py | 4 + 14 files changed, 399 insertions(+), 128 deletions(-) diff --git a/docs/_scripts/_screenshots.py b/docs/_scripts/_screenshots.py index bd7ba18d..4a3e2cb4 100644 --- a/docs/_scripts/_screenshots.py +++ b/docs/_scripts/_screenshots.py @@ -27,7 +27,10 @@ def _write_image(src: str, ns: dict, dest: str) -> None: f"Error evaluating code\n\n{src}\n\nfor {dest!r}" ) from e - canvas = ns["canvas"] + if isinstance(ns.get("grid", None), CanvasGrid): + canvas = ns["grid"] + else: + canvas = ns["canvas"] assert isinstance(canvas, (CanvasGrid, SingleCanvas)), type(canvas) with mkdocs_gen_files.open(dest, "wb") as f: plt.tight_layout() @@ -43,6 +46,7 @@ def main() -> None: theme.canvas_size = (800, 560) theme.font.size = 18 plt.switch_backend("Agg") + names_found = set[str]() for mdfile in sorted(DOCS.rglob("*.md"), reverse=True): if mdfile.name in EXCLUDE: continue @@ -55,15 +59,18 @@ def main() -> None: if code.startswith("#!skip"): continue elif code.startswith("#!name:"): - if code.endswith("canvas.show()"): + if code.endswith(("canvas.show()", "grid.show()")): code = code[:-7] line = code.split("\n", 1)[0] assert line.startswith("#!name:") name = line.split(":", 1)[1].strip() + if name in names_found: + raise ValueError(f"Duplicate name {name!r} in {mdfile}") dest = f"_images/{name}.png" _write_image(code, namespace, dest) + names_found.add(name) else: - if code.endswith("canvas.show()"): + if code.endswith(("canvas.show()", "grid.show()")): code = code[:-7] try: exec(code, namespace, namespace) diff --git a/docs/canvas/grid.md b/docs/canvas/grid.md index 376b06bb..6eec605f 100644 --- a/docs/canvas/grid.md +++ b/docs/canvas/grid.md @@ -1,20 +1,27 @@ # Canvas Grid +A "canvas grid" is a grid of canvases (which is called "figure" in `matplotlib`). +A grid is composed of multiple canvas objects, so that grid itself does not have +either layers or the `add_*` methods. + +Once a grid is created, you can add chid canvases using the `add_canvas` method. +The signature of the method differs between 1D and 2D grid. + ## Vertical/Horizontal Grid ``` python #!name: canvas_grid_vertical from whitecanvas import vgrid -canvas = vgrid(3, backend="matplotlib") +grid = vgrid(3, backend="matplotlib") -c0 = canvas.add_canvas(0) +c0 = grid.add_canvas(0) c0.add_text(0, 0, "Canvas 0") -c1 = canvas.add_canvas(1) +c1 = grid.add_canvas(1) c1.add_text(0, 0, "Canvas 1") -c2 = canvas.add_canvas(2) +c2 = grid.add_canvas(2) c2.add_text(0, 0, "Canvas 2") -canvas.show() +grid.show() ``` @@ -22,29 +29,29 @@ canvas.show() #!name: canvas_grid_horizontal from whitecanvas import hgrid -canvas = hgrid(3, backend="matplotlib") +grid = hgrid(3, backend="matplotlib") -c0 = canvas.add_canvas(0) +c0 = grid.add_canvas(0) c0.add_text(0, 0, "Canvas 0") -c1 = canvas.add_canvas(1) +c1 = grid.add_canvas(1) c1.add_text(0, 0, "Canvas 1") -c2 = canvas.add_canvas(2) +c2 = grid.add_canvas(2) c2.add_text(0, 0, "Canvas 2") -canvas.show() +grid.show() ``` ## 2D Grid ``` python #!name: canvas_grid_2d -from whitecanvas import grid +from whitecanvas import grid as grid2d -canvas = grid(2, 2, backend="matplotlib") +grid = grid2d(2, 2, backend="matplotlib") for i, j in [(0, 0), (0, 1), (1, 0), (1, 1)]: - c = canvas.add_canvas(i, j) + c = grid.add_canvas(i, j) c.add_text(0, 0, f"Canvas ({i}, {j})") -canvas.show() +grid.show() ``` ## Non-uniform Grid @@ -52,15 +59,18 @@ canvas.show() The `*_nonuniform` functions allow you to create a grid with non-uniform sizes. Instead of specifying the number of rows and columns, these functions take a list of size ratios. +!!! note + This feature is work in progress. Some backends does not support it yet. + ``` python #!name: canvas_grid_2d_nonuniform from whitecanvas import grid_nonuniform -canvas = grid_nonuniform([1, 2], [2, 1], backend="matplotlib") +grid = grid_nonuniform([1, 2], [2, 1], backend="matplotlib") for i, j in [(0, 0), (0, 1), (1, 0), (1, 1)]: - c = canvas.add_canvas(i, j) + c = grid.add_canvas(i, j) c.add_text(0, 0, f"Canvas ({i}, {j})") -canvas.show() +grid.show() ``` diff --git a/docs/canvas/namespaces.md b/docs/canvas/namespaces.md index fb959330..5554597e 100644 --- a/docs/canvas/namespaces.md +++ b/docs/canvas/namespaces.md @@ -4,7 +4,7 @@ Each canvas has several namespaces to control the appearance of the canvas. Firs all, the x/y-axis properties are controlled by the `x` and `y` namespaces. ``` python -#!name: xy_axis_properties +#!name: namespace_axis from whitecanvas import new_canvas canvas = new_canvas(backend="matplotlib") @@ -21,7 +21,7 @@ canvas.show() You can set x/y labels using the `label` property. ``` python -#!name: xy_axis_0 +#!name: namespace_axis_label_0 from whitecanvas import new_canvas canvas = new_canvas("matplotlib") @@ -35,7 +35,7 @@ The `label` property is actually another namespace. You can specify the text, fo etc. separately. ``` python -#!name: xy_axis_1 +#!name: namespace_axis_label_1 canvas = new_canvas("matplotlib") canvas.x.label.text = "X axis" @@ -50,7 +50,7 @@ canvas.show() The tick properties can be set via `ticks` property. ``` python -#!name: xy_axis_2 +#!name: namespace_ticks from whitecanvas import new_canvas canvas = new_canvas("matplotlib") @@ -65,7 +65,7 @@ canvas.show() You can also override or reset the tick labels. ``` python -#!name: xy_axis_3 +#!name: namespace_xticks_labels from whitecanvas import new_canvas canvas = new_canvas("matplotlib") @@ -80,7 +80,7 @@ canvas.show() Canvas title can be set via the `title` namespace. ``` python -#!name: xy_axis_2 +#!name: namespace_title from whitecanvas import new_canvas canvas = new_canvas("matplotlib") diff --git a/docs/layers/face_layers.md b/docs/layers/face_layers.md index 0a09e89a..c9758d86 100644 --- a/docs/layers/face_layers.md +++ b/docs/layers/face_layers.md @@ -29,6 +29,10 @@ arguments. You can use the `with_edge` method of the output layer to set edge properties. This separation is very helpful to prevent the confusion of the arguments, especially the colors. +Following example uses [`add_bars`][whitecanvas.canvas.CanvasBase.add_bars] and +[`add_spans`][whitecanvas.canvas.CanvasBase.add_spans] methods to create `Bars` and +`Spans` layers. + ``` python #!name: face_layers_with_edge import numpy as np @@ -36,7 +40,18 @@ from whitecanvas import new_canvas canvas = new_canvas("matplotlib") -layer = canvas.add_markers(np.arange(10), color="yellow").with_edge(color="black") +bars_layer = ( + canvas + .add_bars([0, 1, 2, 3], [3, 4, 1, 2], color="yellow") + .with_edge(color="black") +) + +spans_layer = ( + canvas + .add_spans([[0.2, 0.8], [1.4, 2.1], [1.8, 3.0]], color="blue") + .with_edge(color="black") +) +canvas.y.lim = (0, 5) canvas.show() ``` @@ -44,25 +59,22 @@ All the properties can be set via properties of `face` and `edge`, or the `updat method. ``` python -layer.face.color = "yellow" -layer.face.hatch = "x" +#!skip +bars_layer.face.color = "yellow" +bars_layer.face.hatch = "x" -layer.edge.color = "black" -layer.edge.width = 2 -layer.edge.style = "--" +spans_layer.edge.color = "black" +spans_layer.edge.width = 2 +spans_layer.edge.style = "--" # use `update` -layer.face.update(color="yellow", hatch="x") -layer.edge.update(color="black", width=2, style="--") +bars_layer.face.update(color="yellow", hatch="x") +spans_layer.edge.update(color="black", width=2, style="--") ``` -## Multi-faces and Multi-edges - -`Markers` and `Bars` supports multi-faces and multi-edges. This means that you can -create a layer with multiple colors, widths, etc. +## Multi-face and Multi-edge -To do this, you have to call `with_face_multi` or `with_edge_multi` method. -Here's an example of `Markers` with multi-faces. +As for [`Markers`](markers.md), `Bars` and `Spans` supports multi-face and multi-edge. ``` python #!name: face_layers_multifaces @@ -71,10 +83,10 @@ from whitecanvas import new_canvas canvas = new_canvas("matplotlib") -layer = canvas.add_markers( - np.arange(10), -).with_face_multi( - color=np.random.random((10, 3)), # random colors +layer = ( + canvas + .add_bars([0, 1, 2, 3], [3, 4, 1, 2]) + .with_face_multi(color=["red", "#00FF00", "rgb(0, 0, 255)", "black"]) ) canvas.show() ``` diff --git a/docs/layers/layer_groups.md b/docs/layers/layer_groups.md index b3ed2ebb..5d15bcc1 100644 --- a/docs/layers/layer_groups.md +++ b/docs/layers/layer_groups.md @@ -12,8 +12,15 @@ several built-in layer groups. - `Stem` = `Markers` + `MultiLine` - `Graph` = `Markers` + `MultiLine` + `Texts` -These layer groups can be derived from primitive layers. For example, in the following -code, markers are added to the line at the node positions, resulting in a `Plot` layer. +These layer groups can be derived from primitive layers. It's very important to note +that this layer-grouping architecture makes complex plots to have consistent argument +with the individual plot elements. + +## Layer Groups with Lines + +In this section, we will introduce layer groups that are derived from the `Line` layer. + +### Add markers and/or error bars ``` python #!name: layer_groups_line_markers @@ -21,16 +28,34 @@ from whitecanvas import new_canvas canvas = new_canvas("matplotlib") -canvas.add_line( - [0, 1, 2], [3, 2, 1], color="black", +layer = canvas.add_line( + [0, 1, 2], [3, 2, 4], color="black", name="myplot", ).with_markers( symbol="o", color="red" ) canvas.show() ``` +The `with_markers` method returns a `Plot` layer, which has `Line` and `Markers` as its +children. Therefore, at the very least, any customization can be done on the children. + +``` python +#!skip +print(layer) # Plot<'myplot'> +print(layer.line) # Line<'line-of-myplot'> +print(layer.markers) # Markers<'markers-of-myplot'> +``` + +Once the layer is grouped, layer group instead of the child layers are in the layer +list. + +``` python +#!skip +print(canvas.layers) # LayerList([Plot<'myplot'>]) +``` + The `Plot` layer can be further converted into a `LabeledPlot` layer by adding error -bars using `with_yerr` method. +bars using `with_xerr` and/or `with_yerr` method. ``` python #!name: layer_groups_line_markers_yerr @@ -38,12 +63,142 @@ from whitecanvas import new_canvas canvas = new_canvas("matplotlib") -canvas.add_line( - [0, 1, 2], [3, 2, 1], color="black", -).with_markers( - symbol="o", color="red" -).with_yerr( - [0.1, 0.2, 0.3] +( + canvas + .add_line([0, 1, 2], [3, 2, 4], color="black") + .with_markers(symbol="o", color="red") + .with_yerr([0.1, 0.2, 0.3]) + .with_xerr([0.2, 0.3, 0.2], style="--") +) +canvas.show() +``` + +### Add bands + +[`Band`](face_layers.md) may be used for different purposes. + +1. To fill the area of confidence interval. +2. To fill the area between the line and the x- or y-axis. + +Both cases can be achieved using the `Line` methods. + +To fill the area that represents the errors, such as confidence interval and standard +deviation, use the `with_xband` or `with_yband` method. + +``` python +#!name: layer_groups_line_yband +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") + +# one array for the same size of lower and upper bounds +( + canvas + .add_line([0, 1, 2], [3, 2, 4], color="blue") + .with_yband([0.2, 0.3, 0.4]) +) +# two arrays for different sizes of lower and upper bounds +( + canvas + .add_line([2, 3, 4], [1, 0, 2], color="red") + .with_yband([0.2, 0.3, 0.4], [0.4, 0.6, 0.8]) +) + +canvas.show() +``` + +To fill the area between the line and the axis, use the `with_xfill` or `with_yfill` +respectively. + +``` python +#!name: layer_groups_line_xfill +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") + +( + canvas + .add_line([1, 2, 3], [2.4, 3, 4], color="blue") + .with_xfill() +) +( + canvas + .add_line([2, 3, 4], [2, 1, 2], color="red") + .with_yfill() +) + +canvas.show() +``` + +!!! warning + `with_xfill` fill the area between the line and the **y**-axis. This is because the + orientation of the filling is in the direction of the y-axis, consistent with the + methods such as `with_xband` and `with_xerr`. + +## Layer Groups with Markers + +### Markers with error bars + +[Similar to the `Line` layer](#add-markers-andor-error-bars), the `Markers` layer can +also be grouped with the `Errorbar` layer. + +``` python +#!name: layer_groups_markers_errorbars +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") + +( + canvas + .add_markers([0, 1, 2], [3, 2, 4], color="black", symbol="D", size=10) + .with_xerr([0.2, 0.3, 0.4], style="--") + .with_yerr([0.3, 0.3, 0.5], style=":") +) +canvas.show() +``` + +### Markers as stems + +The `Stem` layer is a layer group of `Markers` and `MultiLine`. It can be created using +the `with_stem` method. + +``` python +#!name: layer_groups_markers_stem +import numpy as np +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") + +x = np.linspace(0, 4 * np.pi, 50) +( + canvas + .add_markers(x, np.sin(x)) + .with_stem() +) +canvas.show() +``` + +### Markers as a graph network + +A network graph is a collection of nodes and edges. The `Graph` layer is a layer group +that can created using the `with_network` method of `Markers`. It uses the list of +index pairs to connect markers. + +``` python +#!name: layer_groups_markers_graph +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") + +nodes = [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0], [0.5, 1.7]] +connections = [[0, 1], [0, 2], [1, 2], [0, 4], [2, 3]] + +( + canvas + .add_markers(nodes, size=40, color="skyblue") + .with_edge(width=2, color="blue") + .with_network(connections) + .with_text(["i=0", "i=1", "i=2", "i=3", "i=4"], size=20) ) canvas.show() ``` diff --git a/docs/layers/lines.md b/docs/layers/lines.md index 46b327c8..b56441b8 100644 --- a/docs/layers/lines.md +++ b/docs/layers/lines.md @@ -109,3 +109,38 @@ canvas.x.lim = (-3, 3) canvas.y.lim = (-3, 3) canvas.show() ``` + +## Errorbars + +`Errorbars` is a layer that represents error bars with caps. It can be created by the +[`add_errorbars`][whitecanvas.canvas.CanvasBase.add_errorbars] method, but if you intend +to add error bars to an existing layer with x/y data, try using the `with_xerr` and +`with_yerr` methods of the layer to [group layers](layer_groups.md). + +``` python +#!name: errorbars_layer +import numpy as np +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") +x = [0, 1, 2] +ylow = [2, 3, 4] +yhigh = [4, 5, 5] +canvas.add_errorbars(x, ylow, yhigh, capsize=0.5, width=3, color="crimson") +canvas.show() +``` + +## Rug + +`Rug` is a layer that represents rug plot (or event plot). It can be created by the +[`add_rug`][whitecanvas.canvas.CanvasBase.add_rug] method. + +``` python +#!name: rug_layer +import numpy as np +from whitecanvas import new_canvas + +canvas = new_canvas("matplotlib") +canvas.add_rug([0.0, 0.1, 0.3, 0.8, 1.4, 3.5], color="black", width=2) +canvas.show() +``` diff --git a/docs/layers/markers.md b/docs/layers/markers.md index 8dc51c0d..1feb6394 100644 --- a/docs/layers/markers.md +++ b/docs/layers/markers.md @@ -236,19 +236,19 @@ rng = np.random.default_rng(999) x = rng.normal(size=1000) y = rng.normal(size=1000) -canvas = hgrid(2, backend="matplotlib") +grid = hgrid(2, backend="matplotlib") ( - canvas + grid .add_canvas(0) .update_labels(title="no coloring") .add_markers(x, y) ) ( - canvas + grid .add_canvas(1) .update_labels(title="with coloring") .add_markers(x, y) .color_by_density(cmap="viridis") ) -canvas.show() +grid.show() ``` diff --git a/whitecanvas/__init__.py b/whitecanvas/__init__.py index f3650be6..ceabc03b 100644 --- a/whitecanvas/__init__.py +++ b/whitecanvas/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0" +__version__ = "0.2.1.dev0" from whitecanvas import theme from whitecanvas.canvas import Canvas, CanvasGrid diff --git a/whitecanvas/canvas/_base.py b/whitecanvas/canvas/_base.py index 15ea8a4e..ad201a5d 100644 --- a/whitecanvas/canvas/_base.py +++ b/whitecanvas/canvas/_base.py @@ -64,6 +64,13 @@ _L0 = TypeVar("_L0", _l.Bars, _l.Band) _void = _Void() +_ATTACH_TO_AXIS = ( + _l.Bars, + _lg.Histogram, + _lg.Kde, + _lg.StemPlot, +) + class CanvasEvents(SignalGroup): lims = Signal(Rect) @@ -112,7 +119,7 @@ def _init_canvas(self): self.layers.events.reordered.connect(self._cb_reordered, unique=True) self.layers.events.connect(self._draw_canvas, unique=True) - self.overlays.events.inserted.connect(self._cb_inserted_overlay, unique=True) + self.overlays.events.inserted.connect(self._cb_overlay_inserted, unique=True) self.overlays.events.removed.connect(self._cb_removed, unique=True) self.overlays.events.connect(self._draw_canvas, unique=True) @@ -1247,9 +1254,11 @@ def add_rug( >>> canvas.add_rug([2, 4, 5, 8, 11]) + ``` │ ││ │ │ ──┴─┴┴──┴───┴──> x 2 45 8 11 + ``` Parameters ---------- @@ -1593,12 +1602,19 @@ def _coerce_name(self, layer_type: type[_l.Layer] | str, name: str | None) -> st i += 1 return name - def _autoscale_for_layer(self, layer: _l.Layer, pad_rel: float = 0.025): + def _autoscale_for_layer( + self, + layer: _l.Layer, + pad_rel: float | None = None, + maybe_empty: bool = True, + ): """This function will be called when a layer is inserted to the canvas.""" + if pad_rel is None: + pad_rel = 0 if isinstance(layer, _l.Image) else 0.025 if not self._autoscale_enabled: return xmin, xmax, ymin, ymax = layer.bbox_hint() - if len(self.layers) > 1: + if len(self.layers) > 1 or not maybe_empty: # NOTE: if there was no layer, so backend may not have xlim/ylim, # or they may be set to a default value. _xmin, _xmax = self.x.lim @@ -1619,7 +1635,12 @@ def _autoscale_for_layer(self, layer: _l.Layer, pad_rel: float = 0.025): xmax += 0.05 else: dx = (xmax - xmin) * pad_rel - xmin -= dx + if ( + xmin != 0 + or not isinstance(layer, _ATTACH_TO_AXIS) + or layer.orient.is_vertical + ): + xmin -= dx xmax += dx if np.isnan(ymax) or np.isnan(ymin): ymin, ymax = self.y.lim @@ -1628,8 +1649,13 @@ def _autoscale_for_layer(self, layer: _l.Layer, pad_rel: float = 0.025): ymax += 0.05 else: dy = (ymax - ymin) * pad_rel - ymin -= dy # TODO: this causes bars/histogram to float - ymax += dy # over the x-axis. + if ( + ymin != 0 + or not isinstance(layer, _ATTACH_TO_AXIS) + or layer.orient.is_horizontal + ): + ymin -= dy + ymax += dy self.lims = xmin, xmax, ymin, ymax def _cb_inserted(self, idx: int, layer: _l.Layer): @@ -1647,14 +1673,10 @@ def _cb_inserted(self, idx: int, layer: _l.Layer): # TODO: check if connecting LayerGroup is necessary layer._connect_canvas(self) # autoscale - if isinstance(layer, _l.Image): - pad_rel = 0 - else: - pad_rel = 0.025 - self._autoscale_for_layer(layer, pad_rel=pad_rel) + self._autoscale_for_layer(layer) self._cb_reordered() - def _cb_inserted_overlay(self, idx: int, layer: _l.Layer): + def _cb_overlay_inserted(self, idx: int, layer: _l.Layer): _canvas = self._canvas() fn = self._get_backend().get("as_overlay") for l in _iter_layers(layer): diff --git a/whitecanvas/layers/_primitive/errorbars.py b/whitecanvas/layers/_primitive/errorbars.py index 0e8343b9..a2c26a62 100644 --- a/whitecanvas/layers/_primitive/errorbars.py +++ b/whitecanvas/layers/_primitive/errorbars.py @@ -92,10 +92,27 @@ def data(self, data): def empty( cls, orient: str | Orientation = Orientation.VERTICAL, + name: str | None = None, backend: Backend | str | None = None, ) -> Errorbars: """Return an Errorbars instance with no component.""" - return Errorbars([], [], [], orient=orient, backend=backend) + return Errorbars([], [], [], name=name, orient=orient, backend=backend) + + @classmethod + def empty_v( + cls, + name: str | None = None, + backend: Backend | str | None = None, + ) -> Errorbars: + """Return a vertical Errorbars instance with no component.""" + return cls.empty(Orientation.VERTICAL, name=name, backend=backend) + + @classmethod + def empty_h( + cls, name: str | None = None, backend: Backend | str | None = None + ) -> Errorbars: + """Return a horizontal Errorbars instance with no component.""" + return cls.empty(Orientation.HORIZONTAL, name=name, backend=backend) def _get_layer_data(self) -> XYYData: """Current data of the layer.""" diff --git a/whitecanvas/layers/_primitive/line.py b/whitecanvas/layers/_primitive/line.py index 051dca44..e2aa612a 100644 --- a/whitecanvas/layers/_primitive/line.py +++ b/whitecanvas/layers/_primitive/line.py @@ -232,9 +232,11 @@ def with_markers( markers = Markers( *self.data, symbol=symbol, size=size, color=color, alpha=alpha, - hatch=hatch, backend=self._backend_name, + hatch=hatch, name=f"markers-of-{self.name}", backend=self._backend_name, ) # fmt: skip - return Plot(self, markers, name=self.name) + old_name = self.name + self.name = f"line-of-{self.name}" + return Plot(self, markers, name=old_name) def with_xerr( self, @@ -262,10 +264,12 @@ def with_xerr( xerr = Errorbars( self.data.y, self.data.x - err, self.data.x + err_high, color=color, width=width, style=style, antialias=antialias, capsize=capsize, - backend=self._backend_name + name=f"xerr-of-{self.name}", backend=self._backend_name ) # fmt: skip - yerr = Errorbars([], [], [], orient="horizontal", backend=self._backend_name) - return LabeledLine(self, xerr, yerr, name=self.name) + yerr = Errorbars.empty_h(f"yerr-of-{self.name}", backend=self._backend_name) + old_name = self.name + self.name = f"line-of-{self.name}" + return LabeledLine(self, xerr, yerr, name=old_name) def with_yerr( self, @@ -293,10 +297,12 @@ def with_yerr( yerr = Errorbars( self.data.x, self.data.y - err, self.data.y + err_high, color=color, width=width, style=style, antialias=antialias, capsize=capsize, - backend=self._backend_name + name=f"yerr-of-{self.name}", backend=self._backend_name ) # fmt: skip - xerr = Errorbars.empty(Orientation.VERTICAL, backend=self._backend_name) - return LabeledLine(self, xerr, yerr, name=self.name) + xerr = Errorbars.empty_v(f"xerr-of-{self.name}", backend=self._backend_name) + old_name = self.name + self.name = f"line-of-{self.name}" + return LabeledLine(self, xerr, yerr, name=old_name) def with_xband( self, @@ -304,7 +310,7 @@ def with_xband( err_high: ArrayLike1D | None = None, *, color: ColorType | _Void = _void, - alpha: float = 0.5, + alpha: float = 0.3, hatch: str | Hatch = Hatch.SOLID, ) -> _lg.LineBand: from whitecanvas.layers._primitive import Band @@ -317,9 +323,12 @@ def with_xband( data = self.data band = Band( data.y, data.x - err, data.x + err_high, orient="horizontal", - color=color, alpha=alpha, hatch=hatch, backend=self._backend_name, + color=color, alpha=alpha, hatch=hatch, name=f"xband-of-{self.name}", + backend=self._backend_name, ) # fmt: skip - return LineBand(self, band, name=self.name) + old_name = self.name + self.name = f"line-of-{self.name}" + return LineBand(self, band, name=old_name) def with_yband( self, @@ -327,7 +336,7 @@ def with_yband( err_high: ArrayLike1D | None = None, *, color: ColorType | _Void = _void, - alpha: float = 0.5, + alpha: float = 0.3, hatch: str | Hatch = Hatch.SOLID, ) -> _lg.LineBand: from whitecanvas.layers._primitive import Band @@ -340,16 +349,19 @@ def with_yband( data = self.data band = Band( data.x, data.y - err, data.y + err_high, orient=Orientation.VERTICAL, - color=color, alpha=alpha, hatch=hatch, backend=self._backend_name, + color=color, alpha=alpha, hatch=hatch, name=f"yband-of-{self.name}", + backend=self._backend_name, ) # fmt: skip - return LineBand(self, band, name=self.name) + old_name = self.name + self.name = f"line-of-{self.name}" + return LineBand(self, band, name=old_name) def with_xfill( self, bottom: float = 0.0, *, color: ColorType | _Void = _void, - alpha: float = 0.5, + alpha: float = 0.3, hatch: str | Hatch = Hatch.SOLID, ) -> _lg.LineBand: from whitecanvas.layers._primitive import Band @@ -360,17 +372,19 @@ def with_xfill( data = self.data x0 = np.full_like(data.x, bottom) band = Band( - data.y, x0, data.x, orient=Orientation.HORIZONTAL, - color=color, alpha=alpha, hatch=hatch, backend=self._backend_name, + data.y, x0, data.x, orient=Orientation.HORIZONTAL, color=color, alpha=alpha, + hatch=hatch, name=f"xfill-of-{self.name}", backend=self._backend_name, ) # fmt: skip - return LineBand(self, band, name=self.name) + old_name = self.name + self.name = f"line-of-{self.name}" + return LineBand(self, band, name=old_name) def with_yfill( self, bottom: float = 0.0, *, color: ColorType | _Void = _void, - alpha: float = 0.5, + alpha: float = 0.3, hatch: str | Hatch = Hatch.SOLID, ) -> _lg.LineBand: from whitecanvas.layers._primitive import Band @@ -381,10 +395,12 @@ def with_yfill( data = self.data y0 = np.full_like(data.y, bottom) band = Band( - data.x, y0, data.y, orient=Orientation.VERTICAL, - color=color, alpha=alpha, hatch=hatch, backend=self._backend_name, + data.x, y0, data.y, orient=Orientation.VERTICAL, color=color, alpha=alpha, + hatch=hatch, name=f"yfill-of-{self.name}", backend=self._backend_name, ) # fmt: skip - return LineBand(self, band, name=self.name) + old_name = self.name + self.name = f"line-of-{self.name}" + return LineBand(self, band, name=old_name) def with_text( self, @@ -394,7 +410,7 @@ def with_text( size: float = 12, rotation: float = 0.0, anchor: str | Alignment = Alignment.BOTTOM_LEFT, - fontfamily: str | None = None, + family: str | None = None, ) -> _lg.LabeledLine: from whitecanvas.layers import Errorbars from whitecanvas.layers.group import LabeledLine @@ -409,21 +425,17 @@ def with_text( f"number of data ({self.data.x.size})." ) texts = Texts( - *self.data, - strings, - color=color, - size=size, - rotation=rotation, - anchor=anchor, - family=fontfamily, - backend=self._backend_name, - ) + *self.data, strings, color=color, size=size, rotation=rotation, + anchor=anchor, family=family, backend=self._backend_name, + ) # fmt: skip + old_name = self.name + self.name = f"line-of-{self.name}" return LabeledLine( self, - Errorbars.empty(Orientation.HORIZONTAL, backend=self._backend_name), - Errorbars.empty(Orientation.VERTICAL, backend=self._backend_name), + Errorbars.empty_h(name=f"xerr-of-{self.name}", backend=self._backend_name), + Errorbars.empty_v(name=f"yerr-of-{self.name}", backend=self._backend_name), texts=texts, - name=self.name, + name=old_name, ) @classmethod diff --git a/whitecanvas/layers/_primitive/markers.py b/whitecanvas/layers/_primitive/markers.py index 17504742..05a8600f 100644 --- a/whitecanvas/layers/_primitive/markers.py +++ b/whitecanvas/layers/_primitive/markers.py @@ -343,9 +343,9 @@ def with_xerr( xerr = Errorbars( self.data.y, self.data.x - err, self.data.x + err_high, color=color, width=width, style=style, antialias=antialias, capsize=capsize, - backend=self._backend_name + orient=Orientation.HORIZONTAL, backend=self._backend_name ) # fmt: skip - yerr = Errorbars.empty(Orientation.VERTICAL, backend=self._backend_name) + yerr = Errorbars.empty_v(f"xerr-of-{self.name}", backend=self._backend_name) return LabeledMarkers(self, xerr, yerr, name=self.name) def with_yerr( @@ -398,9 +398,9 @@ def with_yerr( yerr = Errorbars( self.data.x, self.data.y - err, self.data.y + err_high, color=color, width=width, style=style, antialias=antialias, capsize=capsize, - backend=self._backend_name + orient=Orientation.VERTICAL, backend=self._backend_name ) # fmt: skip - xerr = Errorbars.empty(Orientation.HORIZONTAL, backend=self._backend_name) + xerr = Errorbars.empty_h(f"yerr-of-{self.name}", backend=self._backend_name) return LabeledMarkers(self, xerr, yerr, name=self.name) def with_text( @@ -429,10 +429,11 @@ def with_text( *self.data, strings, color=color, size=size, rotation=rotation, anchor=anchor, family=fontfamily, backend=self._backend_name, ) # fmt: skip + old_name = self.name return LabeledMarkers( self, - Errorbars.empty(Orientation.HORIZONTAL, backend=self._backend_name), - Errorbars.empty(Orientation.VERTICAL, backend=self._backend_name), + Errorbars.empty_h(f"xerr-of-{old_name}", backend=self._backend_name), + Errorbars.empty_v(f"yerr-of-{old_name}", backend=self._backend_name), texts=texts, name=self.name, ) @@ -488,12 +489,9 @@ def with_network( antialias=antialias, backend=self._backend_name ) # fmt: skip texts = Texts( - nodes[:, 0], - nodes[:, 1], - [""] * nodes.shape[0], - name="texts", + nodes[:, 0], nodes[:, 1], [""] * nodes.shape[0], name="texts", backend=self._backend_name, - ) + ) # fmt: skip return Graph(self, edges_layer, texts, edges, name=self.name) def with_stem( diff --git a/whitecanvas/layers/group/labeled.py b/whitecanvas/layers/group/labeled.py index ba420a94..c4c6cc80 100644 --- a/whitecanvas/layers/group/labeled.py +++ b/whitecanvas/layers/group/labeled.py @@ -153,6 +153,8 @@ def with_xerr( color=color, width=width, style=style, antialias=antialias, capsize=capsize, ) # fmt: skip + if canvas := self._canvas_ref(): + canvas._autoscale_for_layer(self.xerr, maybe_empty=False) return self def with_yerr( @@ -184,6 +186,8 @@ def with_yerr( color=color, width=width, style=style, antialias=antialias, capsize=capsize ) # fmt: skip + if canvas := self._canvas_ref(): + canvas._autoscale_for_layer(self.yerr, maybe_empty=False) return self def with_text( @@ -285,19 +289,14 @@ def _init_error_bars( ) -> tuple[Errorbars, Errorbars]: ori = Orientation.parse(orient) errorbar = Errorbars( - x, - est - err, - est + err, - orient=ori, - backend=backend, - capsize=capsize, - ) + x, est - err, est + err, orient=ori, backend=backend, capsize=capsize, + ) # fmt: skip if ori.is_vertical: - xerr = Errorbars.empty(Orientation.HORIZONTAL, backend=backend) + xerr = Errorbars.empty_h(backend=backend) yerr = errorbar else: xerr = errorbar - yerr = Errorbars.empty(Orientation.VERTICAL, backend=backend) + yerr = Errorbars.empty_v(backend=backend) return xerr, yerr diff --git a/whitecanvas/types/_enums.py b/whitecanvas/types/_enums.py index 43d900a5..0d059477 100644 --- a/whitecanvas/types/_enums.py +++ b/whitecanvas/types/_enums.py @@ -166,6 +166,10 @@ def transpose(self): def is_vertical(self): return self is Orientation.VERTICAL + @property + def is_horizontal(self): + return self is Orientation.HORIZONTAL + class Origin(_StrEnum): """