diff --git a/docs/categorical/categorical_axis.md b/docs/categorical/categorical_axis.md index 348d83f5..772c6eed 100644 --- a/docs/categorical/categorical_axis.md +++ b/docs/categorical/categorical_axis.md @@ -126,7 +126,7 @@ canvas ``` ``` python -#!name: categorical_axis_boxplot_0 +#!name: categorical_axis_barplot_0 canvas = new_canvas("matplotlib") canvas.cat(df).add_barplot( offset=["category", "replicate"], diff --git a/whitecanvas/canvas/dataframe/_plot.py b/whitecanvas/canvas/dataframe/_plot.py index 5fc85e21..fe710d6f 100644 --- a/whitecanvas/canvas/dataframe/_plot.py +++ b/whitecanvas/canvas/dataframe/_plot.py @@ -856,19 +856,25 @@ def add_heatmap( canvas.y.ticks.set_labels(*layer._generate_yticks()) return canvas.add_layer(layer) - # TODO: implement this - # def add_2dpointplot( - # self, - # x: str, - # y: str, - # value: str, - # *, - # name: str | None = None, - # color: NStr | None = None, - # width: str | None = None, - # style: NStr | None = None, - # ): - # ... + def add_pointplot2d( + self, + x: str, + y: str, + *, + name: str | None = None, + color: NStr | None = None, + hatch: NStr | None = None, + size: float | None = None, + capsize: float = 0.15, + ): + canvas = self._canvas() + layer = _lt.DFPointPlot2D( + parse(self._df), x, y, name=name, color=color, hatch=hatch, size=size, + capsize=capsize, backend=canvas._get_backend(), + ) # fmt: skip + if self._update_label: + self._update_xy_label(x, y) + return canvas.add_layer(layer) ### Aggregation ### diff --git a/whitecanvas/layers/_base.py b/whitecanvas/layers/_base.py index 6427692b..b6afe6ac 100644 --- a/whitecanvas/layers/_base.py +++ b/whitecanvas/layers/_base.py @@ -278,6 +278,11 @@ def name(self) -> str: def name(self, name: str): self._base_layer.name = name + @property + def base(self) -> _L: + """The base layer.""" + return self._base_layer + def bbox_hint(self) -> NDArray[np.floating]: """Return the bounding box hint using the base layer.""" return self._base_layer.bbox_hint() diff --git a/whitecanvas/layers/group/labeled.py b/whitecanvas/layers/group/labeled.py index ef8ffe0d..3ca144c7 100644 --- a/whitecanvas/layers/group/labeled.py +++ b/whitecanvas/layers/group/labeled.py @@ -257,11 +257,7 @@ def markers(self) -> Markers[_NFace, _NEdge, _Size]: return self._children[0] -def _init_mean_sd( - x, - data, - color, -) -> tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]]: +def _init_mean_sd(x, data, color): x, data = check_array_input(x, data) color = as_color_array(color, len(x)) @@ -436,6 +432,59 @@ def from_arrays( xerr, yerr = _init_error_bars(x, y, err_data, orient, capsize, backend) return cls(plot, xerr=xerr, yerr=yerr, name=name) + @classmethod + def from_arrays_2d( + cls, + xdata: list[ArrayLike1D], + ydata: list[ArrayLike1D], + *, + name: str | None = None, + capsize: float = 0.15, + color: ColorType | list[ColorType] = "blue", + alpha: float = 1.0, + hatch: str | Hatch = Hatch.SOLID, + backend: str | Backend | None = None, + ) -> LabeledPlot[_mixin.MultiFace, _mixin.MultiEdge, float]: + def _estimate(arrs: list[NDArray[np.number]]): + _mean = [] + _sd = [] + for arr in arrs: + _mean.append(np.mean(arr)) + _sd.append(np.std(arr, ddof=1)) + return np.array(_mean), np.array(_sd) + + xmean, xsd = _estimate(xdata) + ymean, ysd = _estimate(ydata) + markers = Markers( + xmean, + ymean, + backend=backend, + ).with_face_multi( + color=color, + hatch=hatch, + alpha=alpha, + ) + lines = Line(xmean, ymean, backend=backend) + plot = Plot(lines, markers) + lines.visible = False + xerr = Errorbars( + ymean, + xmean - xsd, + xmean + xsd, + orient=Orientation.HORIZONTAL, + capsize=capsize, + backend=backend, + ) + yerr = Errorbars( + xmean, + ymean - ysd, + ymean + ysd, + orient=Orientation.VERTICAL, + capsize=capsize, + backend=backend, + ) + return cls(plot, xerr=xerr, yerr=yerr, name=name) + class PlotFace(_mixin.FaceNamespace): _layer: LabeledPlot[_mixin.MultiFace, _mixin.MultiEdge, float] @@ -461,7 +510,7 @@ def hatch(self) -> _mixin.EnumArray[Hatch]: def hatch(self, hatch: str | Hatch | Iterable[str | Hatch]): ndata = self._layer._main_object_layer().ndata hatches = as_any_1d_array(hatch, ndata, dtype=object) - self._layer.markers.with_face_multi(hatch=hatches) + self._layer._main_object_layer().with_face_multi(hatch=hatches) self.events.hatch.emit(hatches) @property diff --git a/whitecanvas/layers/tabular/__init__.py b/whitecanvas/layers/tabular/__init__.py index f83a8706..ecf6cd77 100644 --- a/whitecanvas/layers/tabular/__init__.py +++ b/whitecanvas/layers/tabular/__init__.py @@ -10,6 +10,7 @@ DFLines, DFMarkerGroups, DFMarkers, + DFPointPlot2D, ) __all__ = [ @@ -22,4 +23,5 @@ "DFBars", "DFBoxPlot", "DFHeatmap", + "DFPointPlot2D", ] diff --git a/whitecanvas/layers/tabular/_dataframe.py b/whitecanvas/layers/tabular/_dataframe.py index 3320fcf5..07c557ce 100644 --- a/whitecanvas/layers/tabular/_dataframe.py +++ b/whitecanvas/layers/tabular/_dataframe.py @@ -789,4 +789,28 @@ def _generate_yticks(self): class DFPointPlot2D(_shared.DataFrameLayerWrapper[_lg.LabeledPlot, _DF], Generic[_DF]): - ... + def __init__( + self, + source: DataFrameWrapper[_DF], + x: str, + y: str, + *, + color: str | tuple[str, ...] | None = None, + hatch: str | tuple[str, ...] | None = None, + size: float | None = None, + capsize: float = 0.15, + name: str | None = None, + backend: str | Backend | None = None, + ): + cols = _shared.join_columns(color, hatch, source=source) + xdata = [] + ydata = [] + for _, sub in source.group_by(cols): + xdata.append(sub[x]) + ydata.append(sub[y]) + base = _lg.LabeledPlot.from_arrays_2d( + xdata, ydata, name=name, capsize=capsize, backend=backend + ) + if size is not None: + base.markers.size = size + super().__init__(base, source)