def test_repr(self): assert str(Mappable(.5)) == "<0.5>" assert str(Mappable("CO")) == "<'CO'>" assert str(Mappable(rc="lines.linewidth")) == "<rc:lines.linewidth>" assert str(Mappable(depend="color")) == "<depend:color>" assert str(Mappable(auto=True)) == "<auto>"
def test_depends(self): val = 2 df = pd.DataFrame(index=pd.RangeIndex(10)) m = self.mark(pointsize=Mappable(val), linewidth=Mappable(depend="pointsize")) assert m._resolve({}, "linewidth") == val assert_array_equal(m._resolve(df, "linewidth"), np.full(len(df), val)) m = self.mark(pointsize=val * 2, linewidth=Mappable(depend="pointsize")) assert m._resolve({}, "linewidth") == val * 2 assert_array_equal(m._resolve(df, "linewidth"), np.full(len(df), val * 2))
class Bar(BarBase): """ An rectangular mark drawn between baseline and data values. """ color: MappableColor = Mappable("C0", grouping=False) alpha: MappableFloat = Mappable(.7, grouping=False) fill: MappableBool = Mappable(True, grouping=False) edgecolor: MappableColor = Mappable(depend="color", grouping=False) edgealpha: MappableFloat = Mappable(1, grouping=False) edgewidth: MappableFloat = Mappable(rc="patch.linewidth", grouping=False) edgestyle: MappableStyle = Mappable("-", grouping=False) # pattern: MappableString = Mappable(None) # TODO no Property yet width: MappableFloat = Mappable(.8, grouping=False) baseline: MappableFloat = Mappable(0, grouping=False) # TODO *is* this mappable? def _plot(self, split_gen, scales, orient): val_idx = ["y", "x"].index(orient) for _, data, ax in split_gen(): bars, vals = self._make_patches(data, scales, orient) for bar in bars: # Because we are clipping the artist (see below), the edges end up # looking half as wide as they actually are. I don't love this clumsy # workaround, which is going to cause surprises if you work with the # artists directly. We may need to revisit after feedback. bar.set_linewidth(bar.get_linewidth() * 2) linestyle = bar.get_linestyle() if linestyle[1]: linestyle = (linestyle[0], tuple(x / 2 for x in linestyle[1])) bar.set_linestyle(linestyle) # This is a bit of a hack to handle the fact that the edge lines are # centered on the actual extents of the bar, and overlap when bars are # stacked or dodged. We may discover that this causes problems and needs # to be revisited at some point. Also it should be faster to clip with # a bbox than a path, but I cant't work out how to get the intersection # with the axes bbox. bar.set_clip_path(bar.get_path(), bar.get_transform() + ax.transData) if self.artist_kws.get("clip_on", True): # It seems the above hack undoes the default axes clipping bar.set_clip_box(ax.bbox) bar.sticky_edges[val_idx][:] = (0, np.inf) ax.add_patch(bar) # Add a container which is useful for, e.g. Axes.bar_label if Version(mpl.__version__) >= Version("3.4.0"): orientation = {"x": "vertical", "y": "horizontal"}[orient] container_kws = dict(datavalues=vals, orientation=orientation) else: container_kws = {} container = mpl.container.BarContainer(bars, **container_kws) ax.add_container(container)
class Line(Mark): """ A mark connecting data points with sorting along the orientation axis. """ # TODO other semantics (marker?) color: MappableColor = Mappable("C0", ) alpha: MappableFloat = Mappable(1, ) linewidth: MappableFloat = Mappable(rc="lines.linewidth", ) linestyle: MappableString = Mappable(rc="lines.linestyle", ) # TODO alternately, have Path mark that doesn't sort sort: bool = True def _plot(self, split_gen, scales, orient): for keys, data, ax in split_gen(dropna=False): keys = resolve_properties(self, keys, scales) if self.sort: # TODO where to dropna? data = data.sort_values(orient) else: data.loc[data.isna().any(axis=1), ["x", "y"]] = np.nan line = mpl.lines.Line2D( data["x"].to_numpy(), data["y"].to_numpy(), color=keys["color"], alpha=keys["alpha"], linewidth=keys["linewidth"], linestyle=keys["linestyle"], **self. artist_kws, # TODO keep? remove? be consistent across marks ) ax.add_line(line) def _legend_artist(self, variables, value, scales): key = resolve_properties(self, {v: value for v in variables}, scales) return mpl.lines.Line2D( [], [], color=key["color"], alpha=key["alpha"], linewidth=key["linewidth"], linestyle=key["linestyle"], )
def test_default(self): val = 3 m = self.mark(linewidth=Mappable(val)) assert m._resolve({}, "linewidth") == val df = pd.DataFrame(index=pd.RangeIndex(10)) assert_array_equal(m._resolve(df, "linewidth"), np.full(len(df), val))
def test_fillcolor(self): c, a = "green", .8 fa = .2 m = self.mark( color=c, alpha=a, fillcolor=Mappable(depend="color"), fillalpha=Mappable(fa), ) assert resolve_color(m, {}) == mpl.colors.to_rgba(c, a) assert resolve_color(m, {}, "fill") == mpl.colors.to_rgba(c, fa) df = pd.DataFrame(index=pd.RangeIndex(10)) cs = [c] * len(df) assert_array_equal(resolve_color(m, df), mpl.colors.to_rgba_array(cs, a)) assert_array_equal( resolve_color(m, df, "fill"), mpl.colors.to_rgba_array(cs, fa) )
def test_rcparam(self): param = "lines.linewidth" val = mpl.rcParams[param] m = self.mark(linewidth=Mappable(rc=param)) assert m._resolve({}, "linewidth") == val df = pd.DataFrame(index=pd.RangeIndex(10)) assert_array_equal(m._resolve(df, "linewidth"), np.full(len(df), val))
class MockMark(Mark): linewidth: float = Mappable(rc="lines.linewidth") pointsize: float = Mappable(4) color: str = Mappable("C0") fillcolor: str = Mappable(depend="color") alpha: float = Mappable(1) fillalpha: float = Mappable(depend="alpha")
def test_mapped(self): values = {"a": 1, "b": 2, "c": 3} def f(x): return np.array([values[x_i] for x_i in x]) m = self.mark(linewidth=Mappable(2)) scales = {"linewidth": f} assert m._resolve({"linewidth": "c"}, "linewidth", scales) == 3 df = pd.DataFrame({"linewidth": ["a", "b", "c"]}) expected = np.array([1, 2, 3], float) assert_array_equal(m._resolve(df, "linewidth", scales), expected)
class Dot(Scatter): """ A point mark defined by shape with optional edges. """ marker: MappableString = Mappable("o", grouping=False) color: MappableColor = Mappable("C0", grouping=False) alpha: MappableFloat = Mappable(1, grouping=False) fill: MappableBool = Mappable(True, grouping=False) edgecolor: MappableColor = Mappable(depend="color", grouping=False) edgealpha: MappableFloat = Mappable(depend="alpha", grouping=False) pointsize: MappableFloat = Mappable(6, grouping=False) # TODO rcParam? edgewidth: MappableFloat = Mappable(.5, grouping=False) # TODO rcParam? edgestyle: MappableStyle = Mappable("-", grouping=False) def _resolve_properties(self, data, scales): # TODO this is maybe a little hacky, is there a better abstraction? resolved = super()._resolve_properties(data, scales) filled = resolved["fill"] main_stroke = resolved["stroke"] edge_stroke = resolved["edgewidth"] resolved["linewidth"] = np.where(filled, edge_stroke, main_stroke) # Overwrite the colors that the super class set main_color = resolve_color(self, data, "", scales) edge_color = resolve_color(self, data, "edge", scales) if not np.isscalar(filled): # Expand dims to use in np.where with rgba arrays filled = filled[:, None] resolved["edgecolor"] = np.where(filled, edge_color, main_color) filled = np.squeeze(filled) if isinstance(main_color, tuple): main_color = tuple([*main_color[:3], main_color[3] * filled]) else: main_color = np.c_[main_color[:, :3], main_color[:, 3] * filled] resolved["facecolor"] = main_color return resolved
class Area(AreaBase, Mark): """ An interval mark that fills between baseline and data values. """ color: MappableColor = Mappable("C0", ) alpha: MappableFloat = Mappable(.2, ) fill: MappableBool = Mappable(True, ) edgecolor: MappableColor = Mappable(depend="color") edgealpha: MappableFloat = Mappable(1, ) edgewidth: MappableFloat = Mappable(rc="patch.linewidth", ) edgestyle: MappableStyle = Mappable("-", ) # TODO should this be settable / mappable? baseline: MappableFloat = Mappable(0, grouping=False) def _standardize_coordinate_parameters(self, data, orient): dv = {"x": "y", "y": "x"}[orient] return data.rename(columns={"baseline": f"{dv}min", dv: f"{dv}max"})
def test_color_mapped_alpha(self): c = "r" values = {"a": .2, "b": .5, "c": .8} m = self.mark(color=c, alpha=Mappable(1)) scales = {"alpha": lambda s: np.array([values[s_i] for s_i in s])} assert resolve_color(m, {"alpha": "b"}, "", scales) == mpl.colors.to_rgba(c, .5) df = pd.DataFrame({"alpha": list(values.keys())}) # Do this in two steps for mpl 3.2 compat expected = mpl.colors.to_rgba_array([c] * len(df)) expected[:, 3] = list(values.values()) assert_array_equal(resolve_color(m, df, "", scales), expected)
class Ribbon(AreaBase, Mark): """ An interval mark that fills between minimum and maximum values. """ color: MappableColor = Mappable("C0", ) alpha: MappableFloat = Mappable(.2, ) fill: MappableBool = Mappable(True, ) edgecolor: MappableColor = Mappable(depend="color", ) edgealpha: MappableFloat = Mappable(1, ) edgewidth: MappableFloat = Mappable(0, ) edgestyle: MappableFloat = Mappable("-", ) def _standardize_coordinate_parameters(self, data, orient): # dv = {"x": "y", "y": "x"}[orient] # TODO assert that all(ymax >= ymin)? return data
class Area(AreaBase, Mark): """ An interval mark that fills between baseline and data values. """ color: MappableColor = Mappable("C0", ) alpha: MappableFloat = Mappable(.2, ) fill: MappableBool = Mappable(True, ) edgecolor: MappableColor = Mappable(depend="color") edgealpha: MappableFloat = Mappable(1, ) edgewidth: MappableFloat = Mappable(rc="patch.linewidth", ) edgestyle: MappableStyle = Mappable("-", ) # TODO should this be settable / mappable? baseline: MappableFloat = Mappable(0, grouping=False) def _standardize_coordinate_parameters(self, data, orient): dv = {"x": "y", "y": "x"}[orient] return data.rename(columns={"baseline": f"{dv}min", dv: f"{dv}max"}) def _postprocess_artist(self, artist, ax, orient): # TODO copying a lot of code from Bar, let's abstract this # See comments there, I am not going to repeat them too artist.set_linewidth(artist.get_linewidth() * 2) linestyle = artist.get_linestyle() if linestyle[1]: linestyle = (linestyle[0], tuple(x / 2 for x in linestyle[1])) artist.set_linestyle(linestyle) artist.set_clip_path(artist.get_path(), artist.get_transform() + ax.transData) if self.artist_kws.get("clip_on", True): artist.set_clip_box(ax.bbox) val_idx = ["y", "x"].index(orient) artist.sticky_edges[val_idx][:] = (0, np.inf)
class Path(Mark): """ A mark connecting data points in the order they appear. """ color: MappableColor = Mappable("C0") alpha: MappableFloat = Mappable(1) linewidth: MappableFloat = Mappable(rc="lines.linewidth") linestyle: MappableString = Mappable(rc="lines.linestyle") marker: MappableString = Mappable(rc="lines.marker") pointsize: MappableFloat = Mappable(rc="lines.markersize") fillcolor: MappableColor = Mappable(depend="color") edgecolor: MappableColor = Mappable(depend="color") edgewidth: MappableFloat = Mappable(rc="lines.markeredgewidth") _sort: ClassVar[bool] = False def _plot(self, split_gen, scales, orient): for keys, data, ax in split_gen(keep_na=not self._sort): vals = resolve_properties(self, keys, scales) vals["color"] = resolve_color(self, keys, scales=scales) vals["fillcolor"] = resolve_color(self, keys, prefix="fill", scales=scales) vals["edgecolor"] = resolve_color(self, keys, prefix="edge", scales=scales) # https://github.com/matplotlib/matplotlib/pull/16692 if Version(mpl.__version__) < Version("3.3.0"): vals["marker"] = vals["marker"]._marker if self._sort: data = data.sort_values(orient) artist_kws = self.artist_kws.copy() self._handle_capstyle(artist_kws, vals) line = mpl.lines.Line2D( data["x"].to_numpy(), data["y"].to_numpy(), color=vals["color"], linewidth=vals["linewidth"], linestyle=vals["linestyle"], marker=vals["marker"], markersize=vals["pointsize"], markerfacecolor=vals["fillcolor"], markeredgecolor=vals["edgecolor"], markeredgewidth=vals["edgewidth"], **artist_kws, ) ax.add_line(line) def _legend_artist(self, variables, value, scales): keys = {v: value for v in variables} vals = resolve_properties(self, keys, scales) vals["color"] = resolve_color(self, keys, scales=scales) vals["fillcolor"] = resolve_color(self, keys, prefix="fill", scales=scales) vals["edgecolor"] = resolve_color(self, keys, prefix="edge", scales=scales) # https://github.com/matplotlib/matplotlib/pull/16692 if Version(mpl.__version__) < Version("3.3.0"): vals["marker"] = vals["marker"]._marker artist_kws = self.artist_kws.copy() self._handle_capstyle(artist_kws, vals) return mpl.lines.Line2D( [], [], color=vals["color"], linewidth=vals["linewidth"], linestyle=vals["linestyle"], marker=vals["marker"], markersize=vals["pointsize"], markerfacecolor=vals["fillcolor"], markeredgecolor=vals["edgecolor"], markeredgewidth=vals["edgewidth"], **artist_kws, ) def _handle_capstyle(self, kws, vals): # Work around for this matplotlib issue: # https://github.com/matplotlib/matplotlib/issues/23437 if vals["linestyle"][1] is None: capstyle = kws.get("solid_capstyle", mpl.rcParams["lines.solid_capstyle"]) kws["dash_capstyle"] = capstyle
class Paths(Mark): """ A faster but less-flexible mark for drawing many paths. """ color: MappableColor = Mappable("C0") alpha: MappableFloat = Mappable(1) linewidth: MappableFloat = Mappable(rc="lines.linewidth") linestyle: MappableString = Mappable(rc="lines.linestyle") _sort: ClassVar[bool] = False def __post_init__(self): # LineCollection artists have a capstyle property but don't source its value # from the rc, so we do that manually here. Unfortunately, because we add # only one LineCollection, we have the use the same capstyle for all lines # even when they are dashed. It's a slight inconsistency, but looks fine IMO. self.artist_kws.setdefault("capstyle", mpl.rcParams["lines.solid_capstyle"]) def _setup_lines(self, split_gen, scales, orient): line_data = {} for keys, data, ax in split_gen(keep_na=not self._sort): if ax not in line_data: line_data[ax] = { "segments": [], "colors": [], "linewidths": [], "linestyles": [], } vals = resolve_properties(self, keys, scales) vals["color"] = resolve_color(self, keys, scales=scales) if self._sort: data = data.sort_values(orient) # Column stack to avoid block consolidation xy = np.column_stack([data["x"], data["y"]]) line_data[ax]["segments"].append(xy) line_data[ax]["colors"].append(vals["color"]) line_data[ax]["linewidths"].append(vals["linewidth"]) line_data[ax]["linestyles"].append(vals["linestyle"]) return line_data def _plot(self, split_gen, scales, orient): line_data = self._setup_lines(split_gen, scales, orient) for ax, ax_data in line_data.items(): lines = mpl.collections.LineCollection(**ax_data, **self.artist_kws) # Handle datalim update manually # https://github.com/matplotlib/matplotlib/issues/23129 ax.add_collection(lines, autolim=False) xy = np.concatenate(ax_data["segments"]) ax.update_datalim(xy) def _legend_artist(self, variables, value, scales): key = resolve_properties(self, {v: value for v in variables}, scales) artist_kws = self.artist_kws.copy() capstyle = artist_kws.pop("capstyle") artist_kws["solid_capstyle"] = capstyle artist_kws["dash_capstyle"] = capstyle return mpl.lines.Line2D( [], [], color=key["color"], linewidth=key["linewidth"], linestyle=key["linestyle"], **artist_kws, )
class Scatter(Mark): """ A point mark defined by strokes with optional fills. """ # TODO retype marker as MappableMarker marker: MappableString = Mappable(rc="scatter.marker", grouping=False) stroke: MappableFloat = Mappable(.75, grouping=False) # TODO rcParam? pointsize: MappableFloat = Mappable(3, grouping=False) # TODO rcParam? color: MappableColor = Mappable("C0", grouping=False) alpha: MappableFloat = Mappable(1, grouping=False) # TODO auto alpha? fill: MappableBool = Mappable(True, grouping=False) fillcolor: MappableColor = Mappable(depend="color", grouping=False) fillalpha: MappableFloat = Mappable(.2, grouping=False) def _resolve_paths(self, data): paths = [] path_cache = {} marker = data["marker"] def get_transformed_path(m): return m.get_path().transformed(m.get_transform()) if isinstance(marker, mpl.markers.MarkerStyle): return get_transformed_path(marker) for m in marker: if m not in path_cache: path_cache[m] = get_transformed_path(m) paths.append(path_cache[m]) return paths def _resolve_properties(self, data, scales): resolved = resolve_properties(self, data, scales) resolved["path"] = self._resolve_paths(resolved) if isinstance(data, dict): # TODO need a better way to check filled_marker = resolved["marker"].is_filled() else: filled_marker = [m.is_filled() for m in resolved["marker"]] resolved["linewidth"] = resolved["stroke"] resolved["fill"] = resolved["fill"] * filled_marker resolved["size"] = resolved["pointsize"]**2 resolved["edgecolor"] = resolve_color(self, data, "", scales) resolved["facecolor"] = resolve_color(self, data, "fill", scales) # Because only Dot, and not Scatter, has an edgestyle resolved.setdefault("edgestyle", (0, None)) fc = resolved["facecolor"] if isinstance(fc, tuple): resolved["facecolor"] = fc[0], fc[1], fc[ 2], fc[3] * resolved["fill"] else: fc[:, 3] = fc[:, 3] * resolved["fill"] # TODO Is inplace mod a problem? resolved["facecolor"] = fc return resolved def _plot(self, split_gen, scales, orient): # TODO Not backcompat with allowed (but nonfunctional) univariate plots # (That should be solved upstream by defaulting to "" for unset x/y?) # (Be mindful of xmin/xmax, etc!) for keys, data, ax in split_gen(): offsets = np.column_stack([data["x"], data["y"]]) data = self._resolve_properties(data, scales) points = mpl.collections.PathCollection( offsets=offsets, paths=data["path"], sizes=data["size"], facecolors=data["facecolor"], edgecolors=data["edgecolor"], linewidths=data["linewidth"], linestyles=data["edgestyle"], transOffset=ax.transData, transform=mpl.transforms.IdentityTransform(), ) ax.add_collection(points) def _legend_artist( self, variables: list[str], value: Any, scales: dict[str, Scale], ) -> Artist: key = {v: value for v in variables} res = self._resolve_properties(key, scales) return mpl.collections.PathCollection( paths=[res["path"]], sizes=[res["size"]], facecolors=[res["facecolor"]], edgecolors=[res["edgecolor"]], linewidths=[res["linewidth"]], linestyles=[res["edgestyle"]], transform=mpl.transforms.IdentityTransform(), )
class Bars(BarBase): """ A faster Bar mark with defaults that are more suitable for histograms. """ color: MappableColor = Mappable("C0", grouping=False) alpha: MappableFloat = Mappable(.7, grouping=False) fill: MappableBool = Mappable(True, grouping=False) edgecolor: MappableColor = Mappable(rc="patch.edgecolor", grouping=False) edgealpha: MappableFloat = Mappable(1, grouping=False) edgewidth: MappableFloat = Mappable(auto=True, grouping=False) edgestyle: MappableStyle = Mappable("-", grouping=False) # pattern: MappableString = Mappable(None) # TODO no Property yet width: MappableFloat = Mappable(1, grouping=False) baseline: MappableFloat = Mappable(0, grouping=False) # TODO *is* this mappable? def _plot(self, split_gen, scales, orient): ori_idx = ["x", "y"].index(orient) val_idx = ["y", "x"].index(orient) patches = defaultdict(list) for _, data, ax in split_gen(): bars, _ = self._make_patches(data, scales, orient) patches[ax].extend(bars) collections = {} for ax, ax_patches in patches.items(): col = mpl.collections.PatchCollection(ax_patches, match_original=True) col.sticky_edges[val_idx][:] = (0, np.inf) ax.add_collection(col, autolim=False) collections[ax] = col # Workaround for matplotlib autoscaling bug # https://github.com/matplotlib/matplotlib/issues/11898 # https://github.com/matplotlib/matplotlib/issues/23129 xys = np.vstack([path.vertices for path in col.get_paths()]) ax.update_datalim(xys) if "edgewidth" not in scales and isinstance(self.edgewidth, Mappable): for ax in collections: ax.autoscale_view() def get_dimensions(collection): edges, widths = [], [] for verts in (path.vertices for path in collection.get_paths()): edges.append(min(verts[:, ori_idx])) widths.append(np.ptp(verts[:, ori_idx])) return np.array(edges), np.array(widths) min_width = np.inf for ax, col in collections.items(): edges, widths = get_dimensions(col) points = 72 / ax.figure.dpi * abs( ax.transData.transform([edges + widths] * 2) - ax.transData.transform([edges] * 2) ) min_width = min(min_width, min(points[:, ori_idx])) linewidth = min(.1 * min_width, mpl.rcParams["patch.linewidth"]) for _, col in collections.items(): col.set_linewidth(linewidth)
class Bar(Mark): """ An interval mark drawn between baseline and data values with a width. """ color: MappableColor = Mappable("C0", ) alpha: MappableFloat = Mappable(.7, ) fill: MappableBool = Mappable(True, ) edgecolor: MappableColor = Mappable(depend="color", ) edgealpha: MappableFloat = Mappable(1, ) edgewidth: MappableFloat = Mappable(rc="patch.linewidth") edgestyle: MappableStyle = Mappable("-", ) # pattern: MappableString = Mappable(None, ) # TODO no Property yet width: MappableFloat = Mappable(.8, grouping=False) baseline: MappableFloat = Mappable( 0, grouping=False) # TODO *is* this mappable? def _resolve_properties(self, data, scales): resolved = resolve_properties(self, data, scales) resolved["facecolor"] = resolve_color(self, data, "", scales) resolved["edgecolor"] = resolve_color(self, data, "edge", scales) fc = resolved["facecolor"] if isinstance(fc, tuple): resolved["facecolor"] = fc[0], fc[1], fc[ 2], fc[3] * resolved["fill"] else: fc[:, 3] = fc[:, 3] * resolved["fill"] # TODO Is inplace mod a problem? resolved["facecolor"] = fc return resolved def _plot(self, split_gen, scales, orient): def coords_to_geometry(x, y, w, b): # TODO possible too slow with lots of bars (e.g. dense hist) # Why not just use BarCollection? if orient == "x": w, h = w, y - b xy = x - w / 2, b else: w, h = x - b, w xy = b, y - h / 2 return xy, w, h for _, data, ax in split_gen(): xys = data[["x", "y"]].to_numpy() data = self._resolve_properties(data, scales) bars = [] for i, (x, y) in enumerate(xys): baseline = data["baseline"][i] width = data["width"][i] xy, w, h = coords_to_geometry(x, y, width, baseline) bar = mpl.patches.Rectangle( xy=xy, width=w, height=h, facecolor=data["facecolor"][i], edgecolor=data["edgecolor"][i], linewidth=data["edgewidth"][i], linestyle=data["edgestyle"][i], ) ax.add_patch(bar) bars.append(bar) # TODO add container object to ax, line ax.bar does def _legend_artist( self, variables: list[str], value: Any, scales: dict[str, Scale], ) -> Artist: # TODO return some sensible default? key = {v: value for v in variables} key = self._resolve_properties(key, scales) artist = mpl.patches.Patch( facecolor=key["facecolor"], edgecolor=key["edgecolor"], linewidth=key["edgewidth"], linestyle=key["edgestyle"], ) return artist
class Paths(Mark): """ A faster but less-flexible mark for drawing many paths. """ color: MappableColor = Mappable("C0") alpha: MappableFloat = Mappable(1) linewidth: MappableFloat = Mappable(rc="lines.linewidth") linestyle: MappableString = Mappable(rc="lines.linestyle") _sort: ClassVar[bool] = False def _plot(self, split_gen, scales, orient): line_data = {} for keys, data, ax in split_gen(keep_na=not self._sort): if ax not in line_data: line_data[ax] = { "segments": [], "colors": [], "linewidths": [], "linestyles": [], } vals = resolve_properties(self, keys, scales) vals["color"] = resolve_color(self, keys, scales=scales) if self._sort: data = data.sort_values(orient) # TODO comment about block consolidation xy = np.column_stack([data["x"], data["y"]]) line_data[ax]["segments"].append(xy) line_data[ax]["colors"].append(vals["color"]) line_data[ax]["linewidths"].append(vals["linewidth"]) line_data[ax]["linestyles"].append(vals["linestyle"]) for ax, ax_data in line_data.items(): lines = mpl.collections.LineCollection( **ax_data, **self.artist_kws, ) ax.add_collection(lines, autolim=False) # https://github.com/matplotlib/matplotlib/issues/23129 # TODO get paths from lines object? xy = np.concatenate(ax_data["segments"]) ax.dataLim.update_from_data_xy(xy, ax.ignore_existing_data_limits, updatex=True, updatey=True) def _legend_artist(self, variables, value, scales): key = resolve_properties(self, {v: value for v in variables}, scales) return mpl.lines.Line2D( [], [], color=key["color"], linewidth=key["linewidth"], linestyle=key["linestyle"], **self.artist_kws, )
def test_input_checks(self): with pytest.raises(AssertionError): Mappable(rc="bogus.parameter") with pytest.raises(AssertionError): Mappable(depend="nonexistent_feature")