From 020717a30d31594340bf66b5a2454ece83a40a6a Mon Sep 17 00:00:00 2001 From: Hanjin Liu Date: Sun, 28 Jan 2024 22:05:23 +0900 Subject: [PATCH] fix bar plot --- whitecanvas/backend/bokeh/line.py | 33 ++++++++++-- whitecanvas/canvas/_base.py | 7 ++- whitecanvas/canvas/_stacked.py | 4 +- whitecanvas/canvas/dataframe/_plot.py | 3 +- whitecanvas/layers/_primitive/bars.py | 8 +-- whitecanvas/layers/group/boxplot.py | 2 +- whitecanvas/layers/group/labeled.py | 66 +++++++++++++----------- whitecanvas/layers/tabular/_box_like.py | 11 ++-- whitecanvas/layers/tabular/_dataframe.py | 2 +- whitecanvas/theme/_dataclasses.py | 4 +- 10 files changed, 91 insertions(+), 49 deletions(-) diff --git a/whitecanvas/backend/bokeh/line.py b/whitecanvas/backend/bokeh/line.py index f22c99ee..c239bfa0 100644 --- a/whitecanvas/backend/bokeh/line.py +++ b/whitecanvas/backend/bokeh/line.py @@ -125,8 +125,33 @@ def _plt_set_data(self, data): for seg in data: xdata.append(seg[:, 0]) ydata.append(seg[:, 1]) - self._data.data["x"] = xdata - self._data.data["y"] = ydata + ndata = self._plt_get_ndata() + edge_color = self._data.data["edge_color"] + width = self._data.data["width"] + dash = self._data.data["dash"] + if len(data) < ndata: + loss = ndata - len(data) + edge_color = edge_color[:-loss] + width = width[:-loss] + dash = dash[:-loss] + elif len(data) > ndata: + if ndata == 0: + edge_color = ["blue"] * len(data) + width = [1.0] * len(data) + dash = ["solid"] * len(data) + else: + gain = len(data) - ndata + edge_color = edge_color + edge_color[-1] * gain + width = width + width[-1] * gain + dash = dash + dash[-1] * gain + data = { + "x": xdata, + "y": ydata, + "edge_color": edge_color, + "width": width, + "dash": dash, + } + self._data.data.update(data) def _plt_get_edge_width(self) -> NDArray[np.floating]: return self._data.data["width"] @@ -137,13 +162,13 @@ def _plt_set_edge_width(self, width: float): self._data.data["width"] = width def _plt_get_edge_style(self) -> list[LineStyle]: - return [from_bokeh_line_style(d) for d in self._data.data["style"]] + return [from_bokeh_line_style(d) for d in self._data.data["dash"]] def _plt_set_edge_style(self, style: LineStyle | list[LineStyle]): if isinstance(style, LineStyle): style = [style] * self._plt_get_ndata() val = [to_bokeh_line_style(s) for s in style] - self._data.data["style"] = val + self._data.data["dash"] = val def _plt_get_edge_color(self) -> NDArray[np.float32]: return np.stack([arr_color(c) for c in self._data.data["edge_color"]], axis=0) diff --git a/whitecanvas/canvas/_base.py b/whitecanvas/canvas/_base.py index 8676152e..634180d4 100644 --- a/whitecanvas/canvas/_base.py +++ b/whitecanvas/canvas/_base.py @@ -570,7 +570,7 @@ def add_bars( extent = theme._default("bars.extent", extent) hatch = theme._default("bars.hatch", hatch) layer = _l.Bars( - center, height, bottom, bar_width=extent, name=name, orient=orient, + center, height, bottom, extent=extent, name=name, orient=orient, color=color, alpha=alpha, hatch=hatch, backend=self._get_backend(), ) # fmt: skip return self.add_layer(layer) @@ -1311,6 +1311,8 @@ def _cb_inserted(self, idx: int, layer: _l.Layer): else: pad_rel = 0.025 self._autoscale_for_layer(layer, pad_rel=pad_rel) + if isinstance(layer, (_l.LayerGroup, _l.LayerWrapper)): + self._cb_reordered() def _cb_inserted_overlay(self, idx: int, layer: _l.Layer): _canvas = self._canvas() @@ -1341,6 +1343,9 @@ def _cb_reordered(self): elif isinstance(layer, _l.LayerGroup): for child in layer.iter_children_recursive(): layer_backends.append(child._backend) + elif isinstance(layer, _l.LayerWrapper): + for child in _iter_layers(layer): + layer_backends.append(child._backend) else: raise RuntimeError(f"type {type(layer)} not expected") self._canvas()._plt_reorder_layers(layer_backends) diff --git a/whitecanvas/canvas/_stacked.py b/whitecanvas/canvas/_stacked.py index 4a6b4d2a..e7c6304f 100644 --- a/whitecanvas/canvas/_stacked.py +++ b/whitecanvas/canvas/_stacked.py @@ -70,7 +70,7 @@ def add( alpha=alpha, name=name, hatch=hatch, - bar_width=layer.bar_width, + extent=layer.bar_width, backend=layer._backend_name, ) elif isinstance(layer, Band): @@ -149,7 +149,7 @@ def add_hist( data, bins, density=density, range=(bins.min(), bins.max()) ) new_layer = Bars( - centers, counts, bottom=layer.top, bar_width=dx * 2, name=name, + centers, counts, bottom=layer.top, extent=dx * 2, name=name, color=color, alpha=alpha, orient=layer.orient, hatch=hatch, backend=layer._backend_name, ) # fmt: skip diff --git a/whitecanvas/canvas/dataframe/_plot.py b/whitecanvas/canvas/dataframe/_plot.py index 751f0329..9af35dd1 100644 --- a/whitecanvas/canvas/dataframe/_plot.py +++ b/whitecanvas/canvas/dataframe/_plot.py @@ -429,11 +429,12 @@ def add_barplot( name: str | None = None, orient: _Orientation = Orientation.VERTICAL, capsize: float = 0.1, + extent: float = 0.8, ) -> _lt.WrappedBarPlot[_DF]: canvas = self._canvas() layer = _lt.WrappedBarPlot.from_table( self._df, offset, value, name=name, color=color, hatch=hatch, orient=orient, - capsize=capsize, backend=canvas._get_backend(), + capsize=capsize, extent=extent, backend=canvas._get_backend(), ) # fmt: skip self._post_add_boxlike(layer, color, orient, value) return canvas.add_layer(layer) diff --git a/whitecanvas/layers/_primitive/bars.py b/whitecanvas/layers/_primitive/bars.py index 322d4776..4f11ad2f 100644 --- a/whitecanvas/layers/_primitive/bars.py +++ b/whitecanvas/layers/_primitive/bars.py @@ -72,7 +72,7 @@ def __init__( bottom: ArrayLike1D | None = None, *, orient: str | Orientation = Orientation.VERTICAL, - bar_width: float = 0.8, + extent: float = 0.8, name: str | None = None, color: ColorType = "blue", alpha: float = 1.0, @@ -81,10 +81,10 @@ def __init__( ): MultiFaceEdgeMixin.__init__(self) ori = Orientation.parse(orient) - xxyy, xhint, yhint = _norm_bar_inputs(x, height, bottom, ori, bar_width) + xxyy, xhint, yhint = _norm_bar_inputs(x, height, bottom, ori, extent) super().__init__(name=name) self._backend = self._create_backend(Backend(backend), *xxyy) - self._bar_width = bar_width + self._bar_width = extent self._orient = ori self.face.update(color=color, alpha=alpha, hatch=hatch) self._x_hint, self._y_hint = xhint, yhint @@ -114,7 +114,7 @@ def from_histogram( if bar_width is None: bar_width = edges[1] - edges[0] self = Bars( - centers, counts, bar_width=bar_width, name=name, color=color, alpha=alpha, + centers, counts, extent=bar_width, name=name, color=color, alpha=alpha, orient=orient, hatch=hatch, backend=backend, ) # fmt: skip if density: diff --git a/whitecanvas/layers/group/boxplot.py b/whitecanvas/layers/group/boxplot.py index 1264f105..17e833af 100644 --- a/whitecanvas/layers/group/boxplot.py +++ b/whitecanvas/layers/group/boxplot.py @@ -108,7 +108,7 @@ def from_arrays( agg_arr = np.stack(agg_values, axis=1) box = Bars( x, agg_arr[3] - agg_arr[1], agg_arr[1], name=name, orient=ori, - bar_width=extent, backend=backend, + extent=extent, backend=backend, ).with_face_multi( hatch=hatch, color=color, alpha=alpha, ).with_edge(color="black") # fmt: skip diff --git a/whitecanvas/layers/group/labeled.py b/whitecanvas/layers/group/labeled.py index a7b84725..ef8ffe0d 100644 --- a/whitecanvas/layers/group/labeled.py +++ b/whitecanvas/layers/group/labeled.py @@ -310,7 +310,7 @@ class LabeledBars( _mixin.AbstractFaceEdgeMixin["PlotFace", "PlotEdge"], Generic[_NFace, _NEdge], ): - evens: RichContainerEvents + events: RichContainerEvents _events_class = RichContainerEvents def __init__( @@ -331,6 +331,9 @@ def bars(self) -> Bars: """The bars layer.""" return self._children[0] + def _main_object_layer(self): + return self.bars + def _get_data_xy(self, layer: Bars | None = None) -> tuple[np.ndarray, np.ndarray]: if layer is None: layer = self.bars @@ -352,17 +355,12 @@ def from_arrays( color: ColorType | list[ColorType] = "blue", alpha: float = 1.0, hatch: str | Hatch = Hatch.SOLID, + extent: float = 0.8, backend: str | Backend | None = None, ) -> LabeledBars[_mixin.MultiFace, _mixin.MonoEdge]: x, height, err_data = _init_mean_sd(x, data, color) - bars = Bars( - x, - height, - backend=backend, - ).with_face_multi( - color=color, - hatch=hatch, - alpha=alpha, + bars = Bars(x, height, extent=extent, backend=backend).with_face_multi( + color=color, hatch=hatch, alpha=alpha ) xerr, yerr = _init_error_bars(x, height, err_data, orient, capsize, backend) return cls(bars, xerr=xerr, yerr=yerr, name=name) @@ -404,6 +402,10 @@ def markers(self) -> Markers[_NFace, _NEdge, _Size]: """The markers layer.""" return self.plot.markers + def _main_object_layer(self): + """The main layer with face that will be used in PlotFace/PlotEdge.""" + return self.markers + @classmethod def from_arrays( cls, @@ -441,23 +443,23 @@ class PlotFace(_mixin.FaceNamespace): @property def color(self) -> NDArray[np.floating]: """Face color of the bar.""" - return self._layer.markers.face.color + return self._layer._main_object_layer().face.color @color.setter def color(self, color): - ndata = self._layer.markers.ndata + ndata = self._layer._main_object_layer().ndata col = as_color_array(color, ndata) - self._layer.markers.with_face_multi(color=col) + self._layer._main_object_layer().with_face_multi(color=col) self.events.color.emit(col) @property def hatch(self) -> _mixin.EnumArray[Hatch]: """Face fill hatch.""" - return self._layer.markers.face.hatch + return self._layer._main_object_layer().face.hatch @hatch.setter def hatch(self, hatch: str | Hatch | Iterable[str | Hatch]): - ndata = self._layer.markers.ndata + 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.events.hatch.emit(hatches) @@ -502,53 +504,59 @@ def update( class PlotEdge(_mixin.EdgeNamespace): - _layer: LabeledPlot[_mixin.MultiFace, _mixin.MultiEdge, float] + _layer: LabeledPlot[_NFace, _NEdge, float] | LabeledBars[_NFace, _NEdge] @property def color(self) -> NDArray[np.floating]: """Edge color of the plot.""" - return self._layer.markers.edge.color + return self._layer._main_object_layer().edge.color @color.setter def color(self, color: ColorType): - self._layer.markers.with_edge_multi(color=color) - self._layer.xerr.color = color - self._layer.yerr.color = color + self._layer._main_object_layer().with_edge_multi(color=color) + if self._layer.xerr.ndata > 0: + self._layer.xerr.color = color + if self._layer.yerr.ndata > 0: + self._layer.yerr.color = color self.events.color.emit(color) @property def width(self) -> NDArray[np.float32]: """Edge widths.""" - return self._layer.markers.edge.width + return self._layer._main_object_layer().edge.width @width.setter def width(self, width: float): - self._layer.markers.edge.width = width - self._layer.xerr.width = width - self._layer.yerr.width = width + self._layer._main_object_layer().edge.width = width + if self._layer.xerr.ndata > 0: + self._layer.xerr.width = width + if self._layer.yerr.ndata > 0: + self._layer.yerr.width = width self.events.width.emit(width) @property def style(self) -> _mixin.EnumArray[LineStyle]: """Edge styles.""" - return self._layer.markers.edge.style + return self._layer._main_object_layer().edge.style @style.setter def style(self, style: str | LineStyle): style = LineStyle(style) - self._layer.markers.edge.style = style - self._layer.xerr.style = style - self._layer.yerr.style = style + self._layer._main_object_layer().edge.style = style + if self._layer.xerr.ndata > 0: + self._layer.xerr.style = style + if self._layer.yerr.ndata > 0: + self._layer.yerr.style = style self.events.style.emit(style) @property def alpha(self) -> float: - return self.color[3] + return self.color[:, 3] @alpha.setter def alpha(self, value): color = self.color.copy() - color[3] = value + color[:, 3] = value self.color = color def update( diff --git a/whitecanvas/layers/tabular/_box_like.py b/whitecanvas/layers/tabular/_box_like.py index 6bfdb11d..e9be3521 100644 --- a/whitecanvas/layers/tabular/_box_like.py +++ b/whitecanvas/layers/tabular/_box_like.py @@ -459,13 +459,15 @@ def __init__( name: str | None = None, orient: Orientation = Orientation.VERTICAL, capsize: float = 0.1, + extent: float = 0.8, backend: str | Backend | None = None, ): _BoxLikeMixin.__init__(self, source, offset, value, color, hatch) arrays, self._labels = self._generate_datasets() x = self._offset_by.generate(self._labels, self._splitby) base = _lg.LabeledBars.from_arrays( - x, arrays, name=name, orient=orient, capsize=capsize, backend=backend, + x, arrays, name=name, orient=orient, capsize=capsize, extent=extent, + backend=backend, ) # fmt: skip super().__init__(base, source) base.with_edge(color=theme.get_theme().foreground_color) @@ -486,12 +488,13 @@ def from_table( name: str | None = None, orient: str | Orientation = Orientation.VERTICAL, capsize: float = 0.1, + extent: float = 0.8, backend: str | Backend | None = None, - ) -> WrappedPointPlot[_DF]: + ) -> WrappedBarPlot[_DF]: src = parse(df) - self = WrappedPointPlot( + self = WrappedBarPlot( src, offset, value, orient=orient, name=name, color=color, hatch=hatch, - capsize=capsize, backend=backend + capsize=capsize, extent=extent, backend=backend, ) # fmt: skip return self diff --git a/whitecanvas/layers/tabular/_dataframe.py b/whitecanvas/layers/tabular/_dataframe.py index 45943383..97249540 100644 --- a/whitecanvas/layers/tabular/_dataframe.py +++ b/whitecanvas/layers/tabular/_dataframe.py @@ -525,7 +525,7 @@ def __init__( x = self._offset_by.generate(self._labels, splitby) base = _l.Bars( - x, values, name=name, orient=orient, bar_width=extent, backend=backend + x, values, name=name, orient=orient, extent=extent, backend=backend ).with_face_multi() super().__init__(base, source) if color is not None: diff --git a/whitecanvas/theme/_dataclasses.py b/whitecanvas/theme/_dataclasses.py index 8d5f0e39..58c3b340 100644 --- a/whitecanvas/theme/_dataclasses.py +++ b/whitecanvas/theme/_dataclasses.py @@ -82,7 +82,7 @@ class Font(_BaseModel): class Line(_BaseModel): """Line style.""" - width: float = _field(1.0) + width: float = _field(2.0) style: LineStyle = _field(LineStyle.SOLID) @@ -107,7 +107,7 @@ class Bars(_BaseModel): class ErrorBars(_BaseModel): """Error bar style.""" - width: float = _field(1.0) + width: float = _field(2.0) style: LineStyle = _field(LineStyle.SOLID)