Skip to content
Merged
2 changes: 1 addition & 1 deletion js/src/20_theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const XY_CHROME_CSS = `
:where(.xy [data-xy-slot="tooltip_label"])::after{content:": "}
:where(.xy [data-xy-slot="legend"]){left:var(--xy-legend-left,auto);right:var(--xy-legend-right,auto);top:var(--xy-legend-top,auto);bottom:var(--xy-legend-bottom,auto);transform:var(--xy-legend-transform,none);max-width:var(--xy-legend-max-width);max-height:var(--xy-legend-max-height);gap:2px;font-size:11px;background:var(--chart-legend-bg,rgba(128,128,128,.08));border-radius:4px;padding:4px 8px;color:var(--chart-text,inherit)}
:where(.xy [data-xy-slot="legend_title"]){font-weight:400;text-align:center}
:where(.xy [data-xy-slot="legend_swatch"]){display:inline-block;width:var(--xy-legend-swatch-width,12px);height:var(--xy-legend-swatch-height,10px);vertical-align:-1px;background:var(--xy-legend-swatch-paint,transparent);border-radius:2px;margin-right:5px;fill:var(--xy-legend-swatch-fill);stroke:var(--xy-legend-swatch-stroke);stroke-width:var(--xy-legend-swatch-stroke-width);stroke-dasharray:var(--xy-legend-swatch-dasharray)}
:where(.xy [data-xy-slot="legend_swatch"]){display:inline-block;width:var(--xy-legend-swatch-width,12px);height:var(--xy-legend-swatch-height,10px);vertical-align:-1px;background:var(--xy-legend-swatch-paint,transparent);border-radius:2px;margin-right:var(--xy-legend-swatch-margin-right,5px);fill:var(--xy-legend-swatch-fill);stroke:var(--xy-legend-swatch-stroke);stroke-width:var(--xy-legend-swatch-stroke-width);stroke-dasharray:var(--xy-legend-swatch-dasharray)}
:where(.xy [data-xy-slot="legend_swatch"] > svg){display:block;width:100%;height:100%}
:where(.xy [data-xy-slot="colorbar"]){color:var(--chart-text,inherit);font-size:10px}
:where(.xy [data-xy-slot="colorbar_bar"]){background:var(--xy-colorbar-gradient);border:1px solid currentColor;box-sizing:border-box}
Expand Down
21 changes: 17 additions & 4 deletions js/src/50_chartview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,12 @@ export class ChartView {
const handleHeight = options.handleheight == null
? null
: Math.max(8, 11 * Number(options.handleheight));
const handleLength = Number.isFinite(Number(options.handlelength))
? Math.max(0, Number(options.handlelength))
: 2;
const handleTextPad = Number.isFinite(Number(options.handletextpad))
? Math.max(0, Number(options.handletextpad))
: 0.8;
lg.style.cssText = "position:absolute;" +
`display:grid;grid-template-columns:repeat(${horizontal ? ncols : 1},max-content);` +
"column-gap:2em;row-gap:.5em;overflow:auto;";
Expand Down Expand Up @@ -1921,6 +1927,8 @@ export class ChartView {
// base-layer slot rule. SVG paint lives on the wrapper and inherits into
// its path/line, so Tailwind fill-*/stroke-* utilities on this public slot
// can override it without copying layout classes onto SVG paint nodes.
sw.style.setProperty("--xy-legend-swatch-width", `${handleLength}em`);
sw.style.setProperty("--xy-legend-swatch-margin-right", `${handleTextPad}em`);
let bg = it.swatch;
// A continuous encoding paints the swatch with the colormap ramp, but
// the swatch keeps the mark's identity: a gradient-filled symbol for
Expand All @@ -1938,7 +1946,7 @@ export class ChartView {
const ns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(ns, "svg");
svg.setAttribute("viewBox", "0 0 18 14");
svg.setAttribute("width", "18");
svg.setAttribute("width", "100%");
svg.setAttribute("height", "14");
const path = document.createElementNS(ns, "path");
const paths = {
Expand Down Expand Up @@ -1969,13 +1977,12 @@ export class ChartView {
);
svg.appendChild(path);
sw.appendChild(svg);
sw.style.setProperty("--xy-legend-swatch-width", "18px");
sw.style.setProperty("--xy-legend-swatch-height", "14px");
} else if (it.line) {
const ns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(ns, "svg");
svg.setAttribute("viewBox", "0 0 22 12");
svg.setAttribute("width", "22");
svg.setAttribute("width", "100%");
svg.setAttribute("height", "12");
const ln = document.createElementNS(ns, "line");
ln.setAttribute("x1", "1");
Expand All @@ -1998,7 +2005,6 @@ export class ChartView {
}
svg.appendChild(ln);
sw.appendChild(svg);
sw.style.setProperty("--xy-legend-swatch-width", "22px");
sw.style.setProperty("--xy-legend-swatch-height", "12px");
} else if (it.swatch !== "gradient") {
// Keep the dynamic base paint on the security-audited safe sink, but
Expand All @@ -2008,6 +2014,13 @@ export class ChartView {
"--xy-legend-swatch-paint",
safeCssPaint(this.root, bg),
);
const strokeWidth = Number(it.style?.stroke_width) || 0;
if (it.style?.stroke && strokeWidth > 0) {
sw.style.boxSizing = "border-box";
sw.style.borderStyle = "solid";
sw.style.borderWidth = `${strokeWidth}px`;
sw.style.borderColor = safeCssPaint(this.root, it.style.stroke);
}
// Hatch layers are explicit mark semantics and sit over that sanitized
// base paint without forcing the base color into an inline background.
if (it.style?.hatch) {
Expand Down
11 changes: 10 additions & 1 deletion python/xy/_raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -2362,7 +2362,16 @@ def _emit_legend(
dash=style.get("dash"),
)
else:
cmd.fill(_rect_pts(hx0, cy - swatch_h / 2, hx1, cy + swatch_h / 2), c)
swatch_points = _rect_pts(hx0, cy - swatch_h / 2, hx1, cy + swatch_h / 2)
cmd.fill(swatch_points, c)
stroke_width = max(0.0, float(style.get("stroke_width", 0.0)))
if style.get("stroke") is not None and stroke_width > 0.0:
cmd.stroke(
swatch_points,
stroke_width,
_rgba(style.get("stroke"), color_str),
closed=True,
)
hatch = style.get("hatch")
if hatch:
_emit_legend_hatch(
Expand Down
13 changes: 10 additions & 3 deletions python/xy/_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3772,8 +3772,8 @@ def _legend_layout(named: list[dict], plot: dict, options: dict) -> dict[str, An
# borderpad is applied on both sides, handlelength=2, handletextpad=.8,
# columnspacing=2, and labelspacing=.5 by default.
pad = 2.0 * borderpad * font_size
handle = 2.0 * font_size
gap = 0.8 * font_size
handle = max(0.0, float(options.get("handlelength", 2.0))) * font_size
gap = max(0.0, float(options.get("handletextpad", 0.8))) * font_size
column_gap = 2.0 * font_size
row_gap = labelspacing * font_size
line_h = text_h + row_gap
Expand Down Expand Up @@ -4041,10 +4041,17 @@ def _legend(
f"{_dash_attr(style)}/>"
)
else:
stroke_width = max(0.0, float(style.get("stroke_width", 0.0)))
stroke = style.get("stroke")
stroke_attr = (
f' stroke="{escape(_css(stroke, color))}" stroke-width="{_num(stroke_width)}"'
if stroke is not None and stroke_width > 0.0
else ""
)
rows.append(
f'<rect x="{_num(hx0)}" y="{_num(cy - swatch_h / 2)}" '
f'width="{handle}" height="{_num(swatch_h)}" '
f'rx="2" fill="{escape(color)}"/>'
f'rx="2" fill="{escape(color)}"{stroke_attr}/>'
)
if style.get("hatch"):
rows.append(
Expand Down
23 changes: 21 additions & 2 deletions python/xy/pyplot/_artists.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,13 @@ def __init__(self, artist: Artist, data_line: Optional[Line2D] = None) -> None:
def __iter__(self) -> Iterator[Any]:
return iter(self.lines)

def get_label(self) -> Any:
return self._artist._entry["kwargs"].get("name")

def set_label(self, value: Any) -> None:
self._artist._entry["kwargs"]["name"] = str(value)
self._artist._touch()

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def remove(self) -> None:
self._artist.remove()
self._artist._axes._unregister_container(self)
Expand Down Expand Up @@ -1405,8 +1412,16 @@ def _legend_item_from_entry(
if isinstance(color, str):
style["color"] = color
width = kw.get("width")
if width is not None:
style["width"] = float(width) * point_scale
if width is not None and kind in {"line", "segments", "step", "stairs", "errorbar"}:
# Bar ``width`` is data-space rectangle geometry, not a stroke width.
# Histograms with non-uniform bins therefore carry one width per bar.
# Matplotlib's legend handler draws those as a filled patch and never
# interprets the bin widths as line styling. Generic line collections
# can also expose vector widths; their legend sample uses the first
# linewidth, matching Matplotlib's collection handlers.
widths = np.asarray(width, dtype=np.float64).reshape(-1)
if widths.size:
style["width"] = float(widths[0]) * point_scale
opacity = kw.get("opacity")
if opacity is not None:
style["opacity"] = float(opacity)
Expand Down Expand Up @@ -1441,6 +1456,10 @@ def _legend_item_from_entry(
for key in ("size", "stroke", "stroke_width"):
if kw.get(key) is not None:
style[key] = kw[key]
elif kind not in {"line", "segments", "step", "stairs", "errorbar"}:
for key in ("stroke", "stroke_width"):
if kw.get(key) is not None:
style[key] = kw[key]
return {"name": str(label), "kind": kind, "style": style}


Expand Down
Loading
Loading