|
1 | 1 | from __future__ import annotations
|
2 | 2 | from dataclasses import dataclass
|
3 | 3 |
|
| 4 | +import numpy as np |
4 | 5 | import matplotlib as mpl
|
5 | 6 |
|
6 | 7 | from seaborn._marks.base import (
|
|
13 | 14 | resolve_properties,
|
14 | 15 | resolve_color,
|
15 | 16 | )
|
| 17 | +from seaborn.external.version import Version |
16 | 18 |
|
17 | 19 | from typing import TYPE_CHECKING
|
18 | 20 | if TYPE_CHECKING:
|
@@ -67,31 +69,69 @@ def coords_to_geometry(x, y, w, b):
|
67 | 69 | xy = b, y - h / 2
|
68 | 70 | return xy, w, h
|
69 | 71 |
|
| 72 | + val_idx = ["y", "x"].index(orient) |
| 73 | + |
70 | 74 | for _, data, ax in split_gen():
|
71 | 75 |
|
72 | 76 | xys = data[["x", "y"]].to_numpy()
|
73 | 77 | data = self._resolve_properties(data, scales)
|
74 | 78 |
|
75 |
| - bars = [] |
| 79 | + bars, vals = [], [] |
76 | 80 | for i, (x, y) in enumerate(xys):
|
77 | 81 |
|
78 | 82 | baseline = data["baseline"][i]
|
79 | 83 | width = data["width"][i]
|
80 | 84 | xy, w, h = coords_to_geometry(x, y, width, baseline)
|
81 | 85 |
|
| 86 | + # Skip bars with no value. It's possible we'll want to make this |
| 87 | + # an option (i.e so you have an artist for animating or annotating), |
| 88 | + # but let's keep things simple for now. |
| 89 | + if not np.nan_to_num(h): |
| 90 | + continue |
| 91 | + |
| 92 | + # TODO Because we are clipping the artist (see below), the edges end up |
| 93 | + # looking half as wide as they actually are. I don't love this clumsy |
| 94 | + # workaround, which is going to cause surprises if you work with the |
| 95 | + # artists directly. We may need to revisit after feedback. |
| 96 | + linewidth = data["edgewidth"][i] * 2 |
| 97 | + linestyle = data["edgestyle"][i] |
| 98 | + if linestyle[1]: |
| 99 | + linestyle = (linestyle[0], tuple(x / 2 for x in linestyle[1])) |
| 100 | + |
82 | 101 | bar = mpl.patches.Rectangle(
|
83 | 102 | xy=xy,
|
84 | 103 | width=w,
|
85 | 104 | height=h,
|
86 | 105 | facecolor=data["facecolor"][i],
|
87 | 106 | edgecolor=data["edgecolor"][i],
|
88 |
| - linewidth=data["edgewidth"][i], |
89 |
| - linestyle=data["edgestyle"][i], |
| 107 | + linestyle=linestyle, |
| 108 | + linewidth=linewidth, |
| 109 | + **self.artist_kws, |
90 | 110 | )
|
| 111 | + |
| 112 | + # This is a bit of a hack to handle the fact that the edge lines are |
| 113 | + # centered on the actual extents of the bar, and overlap when bars are |
| 114 | + # stacked or dodged. We may discover that this causes problems and needs |
| 115 | + # to be revisited at some point. Also it should be faster to clip with |
| 116 | + # a bbox than a path, but I cant't work out how to get the intersection |
| 117 | + # with the axes bbox. |
| 118 | + bar.set_clip_path(bar.get_path(), bar.get_transform() + ax.transData) |
| 119 | + if self.artist_kws.get("clip_on", True): |
| 120 | + # It seems the above hack undoes the default axes clipping |
| 121 | + bar.set_clip_box(ax.bbox) |
| 122 | + bar.sticky_edges[val_idx][:] = (0, np.inf) |
91 | 123 | ax.add_patch(bar)
|
92 | 124 | bars.append(bar)
|
| 125 | + vals.append(h) |
93 | 126 |
|
94 |
| - # TODO add container object to ax, line ax.bar does |
| 127 | + # Add a container which is useful for, e.g. Axes.bar_label |
| 128 | + if Version(mpl.__version__) >= Version("3.4.0"): |
| 129 | + orientation = {"x": "vertical", "y": "horizontal"}[orient] |
| 130 | + container_kws = dict(datavalues=vals, orientation=orientation) |
| 131 | + else: |
| 132 | + container_kws = {} |
| 133 | + container = mpl.container.BarContainer(bars, **container_kws) |
| 134 | + ax.add_container(container) |
95 | 135 |
|
96 | 136 | def _legend_artist(
|
97 | 137 | self, variables: list[str], value: Any, scales: dict[str, Scale],
|
|
0 commit comments