Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions benchmarks/test_codspeed_pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,18 @@ def export_data() -> tuple[np.ndarray, np.ndarray]:

# -- paired build arms --------------------------------------------------------
#
# The raw arm mirrors the shim's implicit defaults (explicit x/y axes, the
# 640x480 canvas) so the pair differs only in which API expressed the chart.
# The raw arm mirrors the shim's implicit defaults (explicit x/y axes,
# Matplotlib's 5% line margins, and the 640x480 canvas) so the pair differs
# only in which API expressed the chart.
# The pyplot arm includes plt.close("all") because figure-registry bookkeeping
# is part of the shim's per-figure cost — the exact cost the guardrail bounds.


def _raw_line_payload(x: np.ndarray, y: np.ndarray) -> int:
c = xy.chart(
xy.line(x=x, y=y, color="#1f77b4"),
xy.x_axis(),
xy.y_axis(),
xy.x_axis(margin=0.05),
xy.y_axis(margin=0.05),
width=WIDTH,
height=HEIGHT,
)
Expand Down
4 changes: 3 additions & 1 deletion js/src/00_header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
// v7: `colormap` may be explicit RGB stops rather than a built-in name, and the
// spec may carry a chart `palette`. A v6 client indexes COLORMAP_STOPS with the
// stop array, misses, and paints viridis without erroring.
export const PROTOCOL = 7;
// v8: legend/colorbar geometry, extra colormap names, and match-fill strokes
// add wire values an older v7 client would accept but silently misrender.
export const PROTOCOL = 8;

// HTTP binary frame v1 (spec/design/wire-protocol.md §7; Python side in
// python/xy/_framing.py). The chart spec's PROTOCOL
Expand Down
5 changes: 5 additions & 0 deletions js/src/10_colormaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

const COLORMAP_STOPS = {
binary: [[255, 255, 255], [0, 0, 0]],
reds: [[255, 245, 240], [254, 229, 216], [253, 202, 181], [252, 171, 143], [252, 138, 106], [251, 105, 74], [241, 68, 50], [217, 37, 35], [188, 20, 26], [152, 12, 19], [103, 0, 13]],
bone: [[0, 0, 0], [22, 22, 30], [45, 45, 62], [66, 66, 93], [89, 92, 121], [112, 123, 144], [134, 154, 166], [157, 185, 188], [185, 210, 210], [221, 233, 233], [255, 255, 255]],
autumn: [[255, 0, 0], [255, 25, 0], [255, 51, 0], [255, 76, 0], [255, 102, 0], [255, 128, 0], [255, 153, 0], [255, 179, 0], [255, 204, 0], [255, 230, 0], [255, 255, 0]],
winter: [[0, 0, 255], [0, 25, 242], [0, 51, 230], [0, 76, 217], [0, 102, 204], [0, 128, 191], [0, 153, 178], [0, 179, 166], [0, 204, 153], [0, 230, 140], [0, 255, 128]],
bupu: [[247, 252, 253], [229, 239, 246], [204, 221, 236], [178, 202, 225], [154, 180, 214], [140, 149, 198], [140, 116, 181], [138, 81, 165], [133, 45, 144], [118, 12, 113], [77, 0, 75]],
gray: [[0, 0, 0], [25, 25, 25], [51, 51, 51], [76, 76, 76], [102, 102, 102], [128, 128, 128], [153, 153, 153], [179, 179, 179], [204, 204, 204], [230, 230, 230], [255, 255, 255]],
viridis: [[68, 1, 84], [72, 36, 117], [65, 68, 135], [53, 95, 141], [42, 120, 142], [33, 145, 140], [34, 168, 132], [68, 191, 112], [122, 209, 81], [189, 223, 38], [253, 231, 37]],
plasma: [[13, 8, 135], [65, 4, 157], [106, 0, 168], [143, 13, 164], [177, 42, 144], [204, 71, 120], [225, 100, 98], [242, 132, 75], [252, 166, 54], [252, 206, 37], [240, 249, 33]],
Expand Down
19 changes: 15 additions & 4 deletions js/src/20_theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,28 @@ export function cssColor([r, g, b, a]: any) {
// convention every host we target uses (Reflex/next-themes, Radix Themes,
// Tailwind). These remain zero-specificity :where() rules, so public
// --chart-badge-* / --chart-modebar-* tokens or utility classes override them.
//
// Every chrome text slot below either declares `font-weight:400` or declares
// no weight at all and inherits 400. Matplotlib's `axes.titleweight`,
// `axes.labelweight` and `font.weight` all default to "normal", and its legend
// title and colorbar label are normal too, so 400 is the parity default for
// title, axis titles, annotations, legend titles and colorbar titles alike.
// The SVG (`python/xy/_svg.py`) and native raster (`python/xy/_raster.py`)
// exporters carry the same 400 default — the three renderers must not
// disagree; tests/test_text_weight_defaults.py guards all three. A heavier
// weight is opt-in, via `styles[slot]`, a mark/axis text style, or the pyplot
// `axes.titleweight`/`axes.labelweight` rcParams.
export const XY_CHROME_CSS = `
@layer base{
:where(.xy [data-xy-slot="title"]){text-align:center;font-size:14px;font-weight:600;color:var(--chart-text,inherit)}
:where(.xy [data-xy-slot="title"]){text-align:center;font-size:14px;font-weight:400;color:var(--chart-text,inherit)}
:where(.xy [data-xy-slot="tooltip"]){max-width:calc(100% - 8px);max-height:calc(100% - 8px);box-sizing:border-box;white-space:normal;overflow-wrap:anywhere;overflow:auto;background:var(--chart-tooltip-bg,rgba(20,24,33,.92));color:var(--chart-tooltip-text,#fff);padding:5px 8px;border-radius:4px;font-size:11px;line-height:1.35;box-shadow:0 2px 8px rgba(0,0,0,.3)}
: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:600}
:where(.xy [data-xy-slot="legend_title"]){font-weight:400;text-align:center}
:where(.xy [data-xy-slot="legend_swatch"]){width:12px;height:10px;border-radius:2px;margin-right:5px}
: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}
:where(.xy [data-xy-slot="colorbar_title"]){font-weight:500}
:where(.xy [data-xy-slot="colorbar_title"]){font-weight:400}
:where(.xy [data-xy-slot="badge"]){gap:3px;font-size:11px;line-height:1.2}
:where(.xy [data-xy-slot="badge_item"]){padding:3px 6px;border-radius:4px;color:var(--chart-badge-text,var(--xy-badge-text));background:var(--chart-badge-bg,var(--xy-badge-bg));box-shadow:var(--xy-badge-shadow)}
:where(.xy){--xy-badge-text:#0f172a;--xy-badge-bg:rgba(255,255,255,.82);--xy-badge-shadow:0 1px 4px rgba(15,23,42,.14);--xy-modebar-bg:#fff;--xy-modebar-menu-bg:#fff;--xy-modebar-hover:#edf1f6;--xy-modebar-focus:#1b212a;--xy-modebar-text:#5c6573;--xy-modebar-text-strong:#1b212a;--xy-modebar-text-soft:#798495;--xy-modebar-text-subtle:#9aa4b2;--xy-modebar-border:rgba(27,33,42,.12);--xy-modebar-separator:rgba(27,33,42,.08);--xy-modebar-active:#edf1f6;--xy-modebar-shadow:0 8px 24px rgba(28,32,36,.1),0 2px 6px rgba(28,32,36,.06);--xy-modebar-menu-shadow:0 8px 24px rgba(28,32,36,.12);--xy-modebar-button-shadow:0 1px 2px rgba(28,32,36,.06);--xy-selection:rgba(92,101,115,.6);--xy-selection-fill:rgba(92,101,115,.12)}
Expand Down Expand Up @@ -171,7 +182,7 @@ export const XY_CHROME_CSS = `
:where(.xy [data-xy-slot="crosshair_x"],.xy [data-xy-slot="crosshair_y"]){background:var(--chart-crosshair,rgba(15,23,42,.42))}
:where(.xy [data-xy-slot="tick_label"]){color:var(--chart-text,inherit)}
:where(.xy [data-xy-slot="axis_title"]){color:var(--chart-text,inherit);font-size:12px}
:where(.xy [data-xy-slot="annotation_label"]){font-size:11px;line-height:1.2;font-weight:500;color:var(--chart-annotation-text,var(--chart-text,inherit))}
:where(.xy [data-xy-slot="annotation_label"]){font-size:11px;line-height:1.2;font-weight:400;color:var(--chart-annotation-text,var(--chart-text,inherit))}
:where(.xy [data-xy-slot="canvas"]){cursor:var(--chart-cursor,crosshair)}
:where(.xy [data-xy-slot="canvas"][data-xy-dragmode="pan"]){cursor:var(--chart-cursor-pan,grab)}
:where(.xy [data-xy-slot="canvas"][data-xy-dragmode="none"]){cursor:default}
Expand Down
16 changes: 9 additions & 7 deletions js/src/40_gl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ in vec4 a_rgba; in vec4 a_style; in vec4 a_stroke;
uniform vec2 u_xmap; uniform vec2 u_ymap;
uniform vec2 u_xmeta; uniform vec2 u_ymeta; uniform int u_xmode; uniform float u_xconstant; uniform int u_ymode; uniform float u_yconstant;
uniform float u_size; uniform int u_sizeMode; uniform vec2 u_sizeRange;
uniform int u_colorMode; uniform float u_dpr; uniform int u_selActive;
uniform int u_colorMode; uniform int u_symbol; uniform float u_dpr; uniform int u_selActive;
uniform float u_selectedOpacity; uniform float u_unselectedOpacity;
uniform float u_transitionProgress; uniform int u_transitionActive;
out float v_lutCoord; out float v_dim; out float v_dval; out float v_ptSize; out float v_sel;
Expand All @@ -120,8 +120,10 @@ void main() {
float y = u_transitionActive == 1 ? mix(a_prevy, ay, u_transitionProgress) : ay;
gl_Position = vec4(xyMap(x, u_xmap, u_xmeta, u_xmode, u_xconstant), xyMap(y, u_ymap, u_ymeta, u_ymode, u_yconstant), 0.0, 1.0);
float sz = u_sizeMode == 1 ? mix(u_sizeRange.x, u_sizeRange.y, a_sval) : u_size;
gl_PointSize = sz * u_dpr;
v_ptSize = sz * u_dpr;
int symbol = a_style.w >= 0.0 ? int(a_style.w + 0.5) : u_symbol;
float symbolScale = symbol == 2 || symbol == 14 ? 1.414213562 : 1.0;
gl_PointSize = sz * u_dpr * symbolScale;
v_ptSize = sz * u_dpr * symbolScale;
v_sel = a_sel;
v_rgba = a_rgba;
v_style = a_style;
Expand Down Expand Up @@ -202,8 +204,8 @@ float xyMarkerSdf(vec2 d, int shape) {
if (shape == 3 || shape == 8 || shape == 9 || shape == 10) { // Matplotlib triangle path
vec2 q = d;
if (shape == 8) q = -d;
if (shape == 9) q = vec2(d.y, -d.x);
if (shape == 10) q = vec2(-d.y, d.x);
if (shape == 9) q = vec2(-d.y, d.x);
if (shape == 10) q = vec2(d.y, -d.x);
return xyTriangleDistance(q, vec2(0.0, -0.5), vec2(-0.5, 0.5), vec2(0.5, 0.5));
}
if (shape == 11) { // diagonal x
Expand Down Expand Up @@ -683,7 +685,7 @@ void main() {
float coverage = smoothstep(0.0, max(fwidth(edge) * strokeWidth, 1e-5), edge);
// Both stroke sources ship straight alpha; the per-item alpha stack
// applies to scalar strokes as well (parity with static exporters).
vec4 strokeSrc = u_strokeMode == 1 ? v_stroke : u_stroke;
vec4 strokeSrc = u_strokeMode == 1 ? v_stroke : (u_strokeMode == 2 ? paint : u_stroke);
float strokeAlpha = (v_style.y >= 0.0 ? v_style.y : strokeSrc.a) * v_style.x * u_strokeOpacity;
vec4 stroke = vec4(strokeSrc.rgb * strokeAlpha, strokeAlpha);
outColor = mix(stroke, fill, coverage);
Expand Down Expand Up @@ -920,7 +922,7 @@ void main() {
if (strokeWidth > 0.0) {
// Both stroke sources ship straight alpha; the per-item alpha stack
// applies to scalar strokes as well (parity with static exporters).
vec4 strokeSrc = u_strokeMode == 1 ? v_stroke : u_stroke;
vec4 strokeSrc = u_strokeMode == 1 ? v_stroke : (u_strokeMode == 2 ? paint : u_stroke);
float strokeAlpha = (v_style.y >= 0.0 ? v_style.y : strokeSrc.a) * v_style.x * u_strokeOpacity;
vec4 stroke = vec4(strokeSrc.rgb * strokeAlpha, strokeAlpha);
float inner = 1.0 - smoothstep(-aa, aa, d + strokeWidth);
Expand Down
Loading
Loading