Repository navigation
Fix the audit's 21 P1s across kernel, API, client, exports and pyplot #508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
46d8466
b3c9963
7ce900a
76bb111
6599e2b
346815c
21ef2fc
2b0d1fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,13 +26,39 @@ export function linearTicks(lo, hi, target = 6) { | ||||||||||
| return { ticks: out, step }; | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| // Mirrored by `_log_ticks` in python/xy/_svg.py — the static exporters must | |||||||||||
| // tick a log axis exactly where the browser does (renderer-architecture §6.1). | |||||||||||
| export function logTicks(lo, hi, target = 6) { | |||||||||||
| if (!Number.isFinite(lo) || !Number.isFinite(hi)) return { ticks: [], step: 1 }; | |||||||||||
| const a = Math.min(lo, hi); | |||||||||||
| const b = Math.max(lo, hi); | |||||||||||
| if (a <= 0 || b <= 0) return { ticks: [], step: 1 }; | |||||||||||
| const e0 = Math.floor(Math.log10(a)); | |||||||||||
| const e1 = Math.ceil(Math.log10(b)); | |||||||||||
| // The decade ladder is multiplicative: inside one decade it has at most the | |||||||||||
| // 2 and 5 mantissas to offer, and a window like 0.3..0.35 or 100..110 holds | |||||||||||
| // none of them — every zoom past ~3x on a log axis lost all ticks, labels | |||||||||||
| // and grid. With fewer than two decades in view the ladder cannot describe | |||||||||||
| // the window, so tick it linearly across the span (matplotlib/Plotly do the | |||||||||||
| // same). `step` is the nice linear step so fmtLinear shares one decimal | |||||||||||
| // count along the axis (§6.2); every tick is labelled. | |||||||||||
| let decadesInView = 0; | |||||||||||
| for (let e = e0; e <= e1; e++) { | |||||||||||
| const v = Math.pow(10, e); | |||||||||||
| if (v >= a * (1 - 1e-12) && v <= b * (1 + 1e-12)) decadesInView++; | |||||||||||
| } | |||||||||||
| if (decadesInView < 2) { | |||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change the trigger. Fall back only when fewer than two ladder ticks (the 1/2/5 mantissas) fall in the window, not fewer than two decade ticks. The current test also catches windows that span more than one decade but hold only one power of ten. The 1/2/5 ladder describes those windows well. The linear fallback leaves most of the axis with no ticks and no grid. Evidence (headless Chromium,
On this PR, the left ~60% of the plot (2..20) has no tick and no gridline. The same happens for With the ladder-tick count, Generated by Claude Code |
|||||||||||
| const linear = linearTicks(a, b, target); | |||||||||||
| // A positive subnormal window (1e-323..1.5e-323) is a few ulps wide: | |||||||||||
| // `(b - a) / target` underflows to 0, niceStep answers 1, and the first | |||||||||||
| // multiple of that lies past `b` — no ticks again. The window's own | |||||||||||
| // endpoints are always representable, so they stand in as the ticks | |||||||||||
| // (the formatter is exponential there regardless of step). A degenerate | |||||||||||
| // lo === hi keeps linearTicks' single tick. | |||||||||||
| const ticks = linear.ticks.length < 2 && a < b ? [a, b] : linear.ticks; | |||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: For subnormal windows whose endpoints fall in the same one-decimal exponential bucket, this fallback adds two ticks but formats both as the same label, such as Prompt for AI agents |
|||||||||||
| const step = ticks === linear.ticks ? linear.step : b - a; | |||||||||||
| return { ticks, labels: ticks, step, log: true }; | |||||||||||
| } | |||||||||||
| const span = Math.max(1, e1 - e0); | |||||||||||
| const mults = span <= Math.max(2, target) ? [1, 2, 5] : [1]; | |||||||||||
| const out = []; | |||||||||||
|
|
@@ -258,13 +284,18 @@ function fmtTimeSpec(ms, format) { | ||||||||||
|
|
|||||||||||
| // Decade ticks are multiplicative, so the linear formatter's step-derived | |||||||||||
| // precision rounds every decade under 1.0 to a bare "0" — 0.001 and 0.01 read | |||||||||||
| // as two identical, wrong labels. Label these from their own magnitude. | |||||||||||
| // as two identical, wrong labels. Label these from their own value: the fewest | |||||||||||
| // decimals (up to 8) that reproduce it. For the 1/2/5 ladder that is exactly | |||||||||||
| // the old magnitude rule (0.002 -> 3 decimals, 20 -> none); it differs only | |||||||||||
| // for the within-decade linear ticks a log colorbar now carries (§6.1), which | |||||||||||
| // the magnitude rule collapsed to "2 2 2 3 3 3" for a 2..3 range. | |||||||||||
| // Mirrored by `_fmt_log` in python/xy/_svg.py. | |||||||||||
| export function fmtLog(v) { | |||||||||||
| const av = Math.abs(v); | |||||||||||
| if (av >= 1e6 || (av !== 0 && av < 1e-4)) return v.toExponential(1).replace("e+", "e"); | |||||||||||
| const dec = av && av < 1 ? Math.max(0, Math.ceil(-Math.log10(av))) : 0; | |||||||||||
| return v.toFixed(Math.min(dec, 8)); | |||||||||||
| let dec = 0; | |||||||||||
| while (dec < 8 && Math.abs(Number(av.toFixed(dec)) - av) > av * 1e-9) dec++; | |||||||||||
| return v.toFixed(dec); | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| // Whether a formatted label has lost the value it was meant to show. Tests the | |||||||||||
|
|
@@ -300,8 +331,14 @@ export function fmtAxis(axis, v, tickStep) { | ||||||||||
| const formatted = fmtNumberSpec(v, axis && axis.format); | |||||||||||
| if (axis && axis.scale === "log" && Number(v) > 0 && Number(v) < 1) { | |||||||||||
| // A fixed-decimal spec collapses sub-unit decades; so does the linear | |||||||||||
| // fallback. Either way the magnitude-derived label is the useful one. | |||||||||||
| if (collapsedToZero(formatted)) return fmtLog(v); | |||||||||||
| // fallback under the ladder's step of 1. Either way the value-derived | |||||||||||
| // label is the useful one — except on the within-decade linear tick set | |||||||||||
| // (§6.1), whose sub-unit step gives fmtLinear the one shared decimal | |||||||||||
| // count an axis reads by ("0.30, 0.31, ...") where the per-value fmtLog | |||||||||||
| // would go ragged ("0.3, 0.31, ..."). Mirrored by `_fmt_axis`. | |||||||||||
| if (collapsedToZero(formatted)) { | |||||||||||
| return tickStep > 0 && tickStep < 1 ? fmtLinear(v, tickStep) : fmtLog(v); | |||||||||||
| } | |||||||||||
| } | |||||||||||
| return formatted || fmtLinear(v, tickStep); | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8722,6 +8722,41 @@ export class ChartView { | |
| ]; | ||
| } | ||
|
|
||
| // Whether a legend category toggle removed shipped row `i` from the drawn | ||
| // set. A hidden series is out of every pipeline (interaction spec §10), and | ||
| // the CPU hover fallbacks below scan the UNFILTERED retained columns, so | ||
| // they must ask. `_visInv` is the shipped-row -> drawn-vertex map that | ||
| // `_filterScatterRows` maintains beside `_visMap` (-1 = hidden); reading it, | ||
| // rather than re-deriving the hidden set from the category codes, keeps the | ||
| // fallback in lockstep with what the GPU pick pass can see by construction. | ||
| _cpuRowHidden(g, i) { | ||
| const inv = g._visInv; | ||
| return !!inv && inv[i] < 0; | ||
| } | ||
|
|
||
| // How many retained rows a CPU hover scan covers. `g.n` is the DRAWN count, | ||
| // which a category filter shrinks below the retained columns: capping the | ||
| // scan at it left every visible row shipped after the cut unreachable while | ||
| // hidden rows before it still answered. A filtered trace is therefore scanned | ||
| // over exactly the rows `_visInv` carries visibility for (its length is the | ||
| // pre-filter count `_filterScatterRows` built it from) — deliberately that | ||
| // array and not a separately stored count, so the scan range and the | ||
| // hidden-row test can never disagree. An unfiltered trace keeps the `g.n` | ||
| // cap, which is load-bearing there (a smooth-resampled line's `n` exceeds | ||
| // its source columns; mismatched x/y lengths undershoot them). | ||
| // | ||
| // Growing the retained columns under a live filter would leave both stale, | ||
| // but cannot happen: `_filterScatterRows` runs only on a categorical color | ||
| // channel, and `append_data` (python/xy/interaction.py) rejects exactly | ||
| // those ("append does not support categorical color channels yet"), so a | ||
| // trace is appendable or category-filterable, never both. Whoever lifts that | ||
| // restriction must re-filter on append — `tests/test_legend_hidden_hover.py` | ||
| // fails here when they do. | ||
| _cpuScanLimit(g, ...lengths) { | ||
| const n = g._visInv ? g._visInv.length : g.n; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||
| return Math.min(...lengths, n || lengths[0]); | ||
| } | ||
|
|
||
| _nearestCpuIndex(g, dataX) { | ||
| const cpu = g && g._cpu; | ||
| if (!cpu || !cpu.x || !cpu.x.length) return -1; | ||
|
|
@@ -8730,8 +8765,9 @@ export class ChartView { | |
| const target = this._axisCoord(axis, dataX); | ||
| let best = -1; | ||
| let bestDist = Infinity; | ||
| const limit = Math.min(cpu.x.length, g.n || cpu.x.length); | ||
| const limit = this._cpuScanLimit(g, cpu.x.length); | ||
| for (let i = 0; i < limit; i++) { | ||
| if (this._cpuRowHidden(g, i)) continue; | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| const starts = g._transitionPrevXValues; | ||
| const progress = g._transitionPositionProgress; | ||
| const xEncoded = starts && Number.isFinite(progress) | ||
|
|
@@ -8753,11 +8789,12 @@ export class ChartView { | |
| const xMeta = cpu.xMeta || g.xMeta; | ||
| const yMeta = cpu.yMeta || g.yMeta; | ||
| const progress = g._transitionPositionProgress; | ||
| const limit = Math.min(cpu.x.length, cpu.y.length, g.n || cpu.x.length); | ||
| const limit = this._cpuScanLimit(g, cpu.x.length, cpu.y.length); | ||
| const geom = this._polarGeometry(); | ||
| let best = -1; | ||
| let bestDist = Infinity; | ||
| for (let i = 0; i < limit; i++) { | ||
| if (this._cpuRowHidden(g, i)) continue; | ||
| const xEncoded = g._transitionPrevXValues && Number.isFinite(progress) | ||
| ? g._transitionPrevXValues[i] + (cpu.x[i] - g._transitionPrevXValues[i]) * progress | ||
| : cpu.x[i]; | ||
|
|
@@ -8893,6 +8930,11 @@ export class ChartView { | |
| const cpu = g._cpu; | ||
| const horizontal = g.orientation === 1; | ||
| const geom = this._polarGeometry(); | ||
| // No hidden-row test here: a bar trace cannot carry a categorical color | ||
| // channel (only funnels and point series build one), so it never has | ||
| // category legend rows to toggle and never reaches `_filterScatterRows`. | ||
| // Bar legends are whole-trace rows, which `_hoverAt` skips via | ||
| // `_legendHidden` before it gets here. Recorded in interaction spec §10. | ||
| const limit = Math.min(cpu.x.length, cpu.y.length, g.n || cpu.x.length); | ||
| for (let i = 0; i < limit; i++) { | ||
| const x = this._decodeValue(cpu.x, cpu.xMeta, i); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| Twenty-one P1 defects from the 0.0.7 audit are fixed. Kernel: a non-string | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Remove the internal audit reference ('Twenty-one P1 defects from the 0.0.7 audit') from the changelog text. Describe the user-visible fixes directly, and shorten the entry to a few focused bullets or sentences so it reads like the other fragments. Prompt for AI agents |
||
| message `type`, a pick past a histogram's bin count, and a subnormal-span | ||
| `density_view` window no longer raise inside the dispatcher, and `view_change` | ||
| records only finite, ordered, non-empty ranges. API: a log axis warns when it | ||
| drops non-positive points, non-finite continuous color/size values are not | ||
| drawn instead of painting as the colormap floor, `sankey_chart(xy.sankey(...))` | ||
| works, a hand-built `xy.Mark` fills in factory defaults instead of a KeyError, | ||
| bare pyarrow string arrays become categorical axes, and a missing facet column | ||
| is a ValueError. Client: a log axis zoomed inside one decade shows linear | ||
| fallback ticks instead of none (exports agree), and legend-hidden category | ||
| points no longer hover or show a tooltip. Exports: PDF honors `font-style`, | ||
| `font-family` (mapped to the base-14 faces), `letter-spacing`, and `opacity` | ||
| on text, and XML-illegal control characters are dropped so SVG and PDF never | ||
| emit unparsable documents. pyplot: `ticklabel_format` configures the | ||
| formatter instead of breaking export; spans and rules autoscale and take | ||
| `label=`; `series.plot(ax=)` works with pandas' locators; datetime limits, | ||
| ticks, and `fill_between` coordinates are accepted; `legend(handles=, | ||
| labels=)`, integer and tuple `loc`, `Line2D`/`Patch`/`Rectangle` legend proxies, | ||
| `plt.subplot(n, m, i)` creating only the requested cell, callable `plt.cm.*` | ||
| colormaps, `BarPatch` geometry getters, and a long list of common kwargs | ||
| (`zorder`, `clip_on`, `facecolors='none'`, `tick_label=`, `log=True`, ...) | ||
| are accepted. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This capability note says PNG ignores
font-weightandfont-style, but the raster renderer selects its bold and italic atlas faces. Correct the note and regenerate the capability matrix so PNG users are not told supported styles are vector-only.Prompt for AI agents