Skip to content

bunch of cleanups - #109

Merged
JosephBARBIERDARNAL merged 5 commits into
mainfrom
cleanups
Jul 7, 2026
Merged

bunch of cleanups#109
JosephBARBIERDARNAL merged 5 commits into
mainfrom
cleanups

Conversation

@JosephBARBIERDARNAL

Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Updates CI workflow checkout versions, revises lint and docs command wiring, adds matplotlib as a dependency, removes an unused test constant, expands the NineJS interactive plot configuration, and refreshes the reference docs and generated iframe outputs.

Changes

CI and tooling updates

Layer / File(s) Summary
Checkout action version bumps
.github/workflows/coverage.yml, .github/workflows/lint.yaml, .github/workflows/type.yaml
Updates actions/checkout from v3 to v4 in coverage, lint, and type workflows.
Lint and docs command wiring
.github/workflows/lint.yaml, justfile
Replaces the standalone Ruff format check with a combined Ruff format and lint run, and changes the justfile recipes to run the JS docs build before the docs preview and Python docs steps.
PyPI environment and dependency cleanup
.github/workflows/pypi.yaml, pyproject.toml, tests/test-python/test_main.py
Changes the PyPI publish environment URL, adds matplotlib>=3.10.0 to project dependencies, and removes the unused PLOTNINE_VERSION constant and imports from the Python test file.

Interactive plot and parser runtime

Layer / File(s) Summary
Interactive API and plot data
ninejs/main.py
interactive and _InteractivePlot accept new zoom and nearest-hover sampling parameters, store them, and serialize them into plot_data_json.
Parser, hover, nearest-hover, and zoom runtime
ninejs/static/PlotParser.js, ninejs/static/PlotParserGeometry.js, ninejs/static/PlotParserHover.js, ninejs/static/PlotParserInit.js, ninejs/static/PlotParserNearestHover.js, ninejs/static/PlotParserZoom.js
PlotSVGParser and the related geometry, hover, nearest-hover, and zoom helpers now use the new nearest-hover sampling and zoom configuration fields, with added parser methods and updated zoom reset timing.

Documentation and generated iframe refresh

Layer / File(s) Summary
Reference docs and helper comments
docs/javascript-reference.md, docs/javascripts/llms.js, docs/contributing.md
Expands the JavaScript reference, adds JSDoc comments to the docs helper script, and adds a contributing link for the JavaScript reference.
Iframe payload and runtime refresh
docs/iframes/*
Regenerates the iframe HTML outputs, updates embedded plot-data with nearest-hover and zoom settings, and replaces the inline NineJS runtime code to use the new configuration flow.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • y-sunflower/ninejs#43: Updates call sites to match the PlotParser summary/signature changes used by the same parser layer.
  • y-sunflower/ninejs#46: Also threads nearest-hover behavior through the NineJS parsing and interaction path.
  • y-sunflower/ninejs#84: Shares the initPlot() browser initialization path that now reads and forwards plot_data configuration.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too vague and generic to identify the main change in this large workflow, docs, and code cleanup PR. Use a concise title that names the primary change, such as updated NineJS plotting docs, linting, and workflow cleanup.
Description check ❓ Inconclusive No pull request description was provided, so there isn't enough text to assess intent beyond the file changes. Add a short description summarizing the purpose of the cleanup and the main areas touched.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cleanups

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
docs/iframes/animation-art.html

ast-grep timed out on this file

docs/iframes/animation-line.html

ast-grep retry budget exhausted before isolating this batch

docs/iframes/animation.html

ast-grep retry budget exhausted before isolating this batch

  • 11 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
.github/workflows/coverage.yml (1)

14-15: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout.

Static analysis flags credential persistence via GitHub Actions artifacts on this checkout step. Setting persist-credentials: false avoids leaving the GITHUB_TOKEN in the local git config for subsequent steps that don't need it.

🔒 Suggested fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/coverage.yml around lines 14 - 15, The checkout step in
the coverage workflow currently leaves the GitHub token persisted in local git
config; update the actions/checkout usage in the workflow’s checkout step to
disable credential persistence. Keep the existing Checkout repository step, but
add persist-credentials: false so subsequent steps do not inherit unnecessary
repository credentials.

Source: Linters/SAST tools

.github/workflows/lint.yaml (1)

14-15: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout.

Same artipacked finding as in the other workflows — the GITHUB_TOKEN persists in git config unnecessarily.

🔒 Suggested fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/lint.yaml around lines 14 - 15, The checkout step is still
persisting the GITHUB_TOKEN in git config, so update the actions/checkout usage
in the workflow to disable credential persistence. Adjust the Checkout
repository step to include the persist-credentials setting for the checkout
action, and keep the change scoped to the workflow job where this checkout
occurs.

Source: Linters/SAST tools

.github/workflows/type.yaml (1)

14-15: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout.

Same artipacked finding as in the other workflows.

🔒 Suggested fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/type.yaml around lines 14 - 15, The checkout step in the
workflow should disable persisted Git credentials to match the other workflow
fixes. Update the actions/checkout usage in the checkout step to set
persist-credentials to false so the job does not retain repository auth after
checkout.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/coverage.yml:
- Around line 14-15: The checkout step in the coverage workflow currently leaves
the GitHub token persisted in local git config; update the actions/checkout
usage in the workflow’s checkout step to disable credential persistence. Keep
the existing Checkout repository step, but add persist-credentials: false so
subsequent steps do not inherit unnecessary repository credentials.

In @.github/workflows/lint.yaml:
- Around line 14-15: The checkout step is still persisting the GITHUB_TOKEN in
git config, so update the actions/checkout usage in the workflow to disable
credential persistence. Adjust the Checkout repository step to include the
persist-credentials setting for the checkout action, and keep the change scoped
to the workflow job where this checkout occurs.

In @.github/workflows/type.yaml:
- Around line 14-15: The checkout step in the workflow should disable persisted
Git credentials to match the other workflow fixes. Update the actions/checkout
usage in the checkout step to set persist-credentials to false so the job does
not retain repository auth after checkout.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 997639f3-8109-4f2d-a6fc-58cf288b84e4

📥 Commits

Reviewing files that changed from the base of the PR and between 3ceb753 and 40a6fb4.

📒 Files selected for processing (7)
  • .github/workflows/coverage.yml
  • .github/workflows/lint.yaml
  • .github/workflows/pypi.yaml
  • .github/workflows/type.yaml
  • justfile
  • pyproject.toml
  • tests/test-python/test_main.py
💤 Files with no reviewable changes (1)
  • tests/test-python/test_main.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/iframes/point.html`:
- Line 15: point.html is still using an outdated embedded NineJS runtime and
missing the newer plot-data options, so regenerate the iframe with the shared
runtime bundle. Update the runtime used by initPlot/PlotSVGParser so it picks up
the same schema support as sibling iframes, including nearest_sample_spacing,
nearest_max_samples, zoom_max_scale, zoom_reset_duration, and hover_keys.

In `@ninejs/main.py`:
- Around line 73-76: Add validation for the new zoom/nearest-hover parameters in
the entrypoint that forwards them into plot_data_json, since invalid values
currently propagate into PlotParserZoom.js and nearest-hover sampling. In the
function that defines zoom_max_scale, zoom_reset_duration,
nearest_sample_spacing, and nearest_max_samples, reject or clamp zoom_max_scale
values at or below 1, and ensure nearest_sample_spacing and nearest_max_samples
are positive before calling plot_data_json; keep the check close to the
parameter parsing/dispatch so users get an immediate error instead of a broken
plot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4915c919-ecc6-4728-bf0f-ede6c69997ac

📥 Commits

Reviewing files that changed from the base of the PR and between 40a6fb4 and da6a5c3.

⛔ Files ignored due to path filters (4)
  • bun.lock is excluded by !**/*.lock
  • ninejs/static/PlotParser.min.js is excluded by !**/*.min.js
  • package-lock.json is excluded by !**/package-lock.json
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (52)
  • .github/workflows/coverage.yml
  • .github/workflows/lint.yaml
  • .github/workflows/type.yaml
  • docs/contributing.md
  • docs/iframes/animation-art.html
  • docs/iframes/animation-hover.html
  • docs/iframes/animation-line.html
  • docs/iframes/animation.html
  • docs/iframes/area-chart.html
  • docs/iframes/area-hover-to-show.html
  • docs/iframes/bar.html
  • docs/iframes/climate-vulnerability-co2.html
  • docs/iframes/coal-production.html
  • docs/iframes/effects-confetti.html
  • docs/iframes/europe-co2.html
  • docs/iframes/facet_wrap.html
  • docs/iframes/household-wealth.html
  • docs/iframes/hover-nearest.html
  • docs/iframes/javascript-animate.html
  • docs/iframes/javascript-click-color.html
  • docs/iframes/javascript-hello.html
  • docs/iframes/linked-map-bars.html
  • docs/iframes/matplotlib-annotation.html
  • docs/iframes/mondrian.html
  • docs/iframes/mtcars-linked-panels.html
  • docs/iframes/on-click-custom-alert.html
  • docs/iframes/on-click-new-window.html
  • docs/iframes/on-click-stroke.html
  • docs/iframes/point.html
  • docs/iframes/quickstart2.html
  • docs/iframes/sao-paulo-hdi.html
  • docs/iframes/saving-rate.html
  • docs/iframes/south-france-restaurants.html
  • docs/iframes/tooltip-after-stat.html
  • docs/iframes/tooltip-custom-css.html
  • docs/iframes/tooltip-custom-css2.html
  • docs/iframes/tooltip-default.html
  • docs/iframes/tooltip-html-injection.html
  • docs/iframes/zoom.html
  • docs/javascript-reference.md
  • docs/javascripts/llms.js
  • justfile
  • ninejs/main.py
  • ninejs/static/PlotParser.js
  • ninejs/static/PlotParserGeometry.js
  • ninejs/static/PlotParserHover.js
  • ninejs/static/PlotParserInit.js
  • ninejs/static/PlotParserNearestHover.js
  • ninejs/static/PlotParserZoom.js
  • package.json
  • tests/test-javascript/PlotParser.test.js
  • zensical.toml
✅ Files skipped from review due to trivial changes (6)
  • .github/workflows/type.yaml
  • docs/javascripts/llms.js
  • docs/contributing.md
  • ninejs/static/PlotParserNearestHover.js
  • ninejs/static/PlotParserHover.js
  • docs/javascript-reference.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/lint.yaml

Comment thread docs/iframes/point.html
// prettier-ignore
// ninejs-sources-hash: 7ff57cec105ee9eab5039985552dced2517705a376aea09491705ae52fd3b582
function getNodeAnchorPoints(t,e){if(e.classList?.contains("point"))return getBBoxAnchorPoints(t,e,!1);const n=getPathSamplePoints(t,e);return n.length>0?n:getBBoxAnchorPoints(t,e,!0)}function getPathSamplePoints(t,e){if("function"!=typeof e.getTotalLength||"function"!=typeof e.getPointAtLength)return[];let n;try{n=e.getTotalLength()}catch{return[]}if(!Number.isFinite(n)||n<=0)return[];const o=Math.max(1,Math.min(t.max_path_samples,Math.ceil(n/t.path_sample_spacing))),i=[];for(let r=0;r<=o;r+=1)try{const s=e.getPointAtLength(n*r/o);i.push(nodePointToSvg(t,e,s.x,s.y))}catch{return i}return i}function getBBoxAnchorPoints(t,e,n){const o=getNodeBBox(t,e);if(!o)return[];const i=o.x+o.width/2,r=o.y+o.height/2,s=[{x:i,y:r}];return n&&s.push({x:o.x,y:o.y},{x:o.x+o.width,y:o.y},{x:o.x,y:o.y+o.height},{x:o.x+o.width,y:o.y+o.height},{x:i,y:o.y},{x:i,y:o.y+o.height},{x:o.x,y:r},{x:o.x+o.width,y:r}),s.map(n=>nodePointToSvg(t,e,n.x,n.y))}function getNodeBBox(t,e){if("function"==typeof e.getBBox)try{const t=e.getBBox();if(isFiniteBBox(t))return t}catch{}return getAttributeBBox(e)}function getAttributeBBox(t){const e=numberAttribute(t,"x"),n=numberAttribute(t,"y"),o=numberAttribute(t,"width"),i=numberAttribute(t,"height");if(Number.isFinite(e)&&Number.isFinite(n)&&Number.isFinite(o)&&Number.isFinite(i))return{x:e,y:n,width:o,height:i};const r=numberAttribute(t,"cx"),s=numberAttribute(t,"cy"),l=numberAttribute(t,"r");return Number.isFinite(r)&&Number.isFinite(s)&&Number.isFinite(l)?{x:r-l,y:s-l,width:2*l,height:2*l}:null}function numberAttribute(t,e){const n=t.getAttribute(e);if(null===n)return NaN;const o=Number(n);return Number.isFinite(o)?o:NaN}function getPanelBounds(t,e){const n=getAxesClipBounds(t,e);if(n)return n;const o=t.svg.select(`g#${e}`).node();return o?getNodeBBox(t,o):null}function getAxesClipBounds(t,e){const n=t.svg.select(`g#${e}`).node(),o=n?.querySelector("[clip-path]"),i=o?.getAttribute("clip-path"),r=getClipPathId(i);if(!r)return null;const s=t.svg.node().ownerDocument.getElementById(r),l=s?.querySelector("rect");return l?getAttributeBBox(l):null}function getClipPathId(t){if(!t)return null;const e=/url\(["']?#([^)"']+)["']?\)/.exec(t);return e?e[1]:null}function eventToSvgPoint(t,e){const n=e.clientX??e.pageX,o=e.clientY??e.pageY;return Number.isFinite(n)&&Number.isFinite(o)?clientPointToSvg(t,n,o):null}function clientPointToSvg(t,e,n){const o=t.svg.node();if(o&&"function"==typeof o.createSVGPoint&&"function"==typeof o.getScreenCTM)try{const t=o.getScreenCTM();if(t){const i=o.createSVGPoint();return i.x=e,i.y=n,i.matrixTransform(t.inverse())}}catch{}return clientPointToSvgFromViewBox(t,e,n)}function clientPointToSvgFromViewBox(t,e,n){const o=t.svg.node();if(!o||"function"!=typeof o.getBoundingClientRect)return{x:e,y:n};const i=o.getBoundingClientRect(),r=getSvgViewBox(o,i);return r&&Number.isFinite(i.width)&&Number.isFinite(i.height)&&0!==i.width&&0!==i.height?{x:r.x+(e-i.left)*r.width/i.width,y:r.y+(n-i.top)*r.height/i.height}:{x:e,y:n}}function getSvgViewBox(t,e){const n=t.viewBox?.baseVal;if(n&&n.width>0&&n.height>0)return n;const o=t.getAttribute("viewBox");if(o){const t=o.trim().split(/[\s,]+/).map(t=>Number(t));if(4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>0&&t[3]>0)return{x:t[0],y:t[1],width:t[2],height:t[3]}}return e.width>0&&e.height>0?{x:0,y:0,width:e.width,height:e.height}:null}function nodePointToSvg(t,e,n,o){const i=t.svg.node();if(i&&"function"==typeof i.createSVGPoint&&"function"==typeof i.getScreenCTM&&"function"==typeof e.getScreenCTM)try{const t=e.getScreenCTM(),r=i.getScreenCTM();if(t&&r){const e=i.createSVGPoint();return e.x=n,e.y=o,e.matrixTransform(t).matrixTransform(r.inverse())}}catch{}if(i&&"function"==typeof i.createSVGPoint&&"function"==typeof i.getCTM&&"function"==typeof e.getCTM)try{const t=e.getCTM(),r=i.getCTM();if(t&&r){const e=i.createSVGPoint();return e.x=n,e.y=o,e.matrixTransform(t).matrixTransform(r.inverse())}}catch{}return{x:n,y:o}}function isFinitePoint(t){return t&&Number.isFinite(t.x)&&Number.isFinite(t.y)}function isFiniteBBox(t){return t&&Number.isFinite(t.x)&&Number.isFinite(t.y)&&Number.isFinite(t.width)&&Number.isFinite(t.height)}function pointInBounds(t,e){return t.x>=e.x&&t.x<=e.x+e.width&&t.y>=e.y&&t.y<=e.y+e.height}function setTooltipContent(t,e){const n=null==e?"":String(e);t.sanitizer&&"function"==typeof t.sanitizer.sanitize?t.tooltip.html(t.sanitizer.sanitize(n,t.tooltip_sanitize_config)):t.tooltip.text(n)}function repeatExact(t,e){return 0===t.length||t.length===e?t:e>t.length&&e%t.length===0?Array.from({length:e},(e,n)=>t[n%t.length]):t}function hasClickHandler(t){return null!=t&&(("number"!=typeof t||!Number.isNaN(t))&&""!==String(t).trim())}function getClickHandler(t){if("function"==typeof t)return t;if(!hasClickHandler(t))return null;const e=globalThis.ninejs?.clickHandlers;if(!e)return null;const n=e[String(t)];return"function"==typeof n?n:null}function normalizeHoverConfig(t,e){const n=repeatExact(t.tooltipLabels||[],e);let o=repeatExact(t.tooltipGroups||[],e);const i=repeatExact(t.clickHandlers||[],e);return 0===o.length&&(n.length>0||i.length>0)&&(o=Array.from({length:e},(t,e)=>e)),{...t,tooltipLabels:n,tooltipGroups:o,clickHandlers:i}}function clearHoverEffects(t){for(const e of t)e.plotElements.classed("not-hovered",!1).classed("hovered",!1)}function positionTooltip(t,e,n){t.tooltip.style("display",n).style("left",e.pageX+"px").style("top",e.pageY+"px")}function applyHoverRecord(t,e,n,o){const i=e.hoverConfig,r=i.tooltipGroups,s=r[e.index];clearHoverEffects(o);const l=i.plotElements.filter((t,e)=>r[e]===s);i.reverseHover?l.classed("not-hovered",!0):(i.plotElements.classed("not-hovered",!0),l.classed("not-hovered",!1).classed("hovered",!0)),positionTooltip(t,n,i.showTooltip),setTooltipContent(t,i.tooltipLabels[e.index])}function setHoverEffect(t,e,n,o,i,r=!1,s=[]){const l=e.nodes(),c=normalizeHoverConfig({plotElements:e,tooltipLabels:n,tooltipGroups:o,showTooltip:i,reverseHover:r,clickHandlers:s},l.length),a=[c];setClickEffect(t,e,c.clickHandlers),e.on("mouseover",function(e){const n=l.indexOf(this);applyHoverRecord(t,{hoverConfig:c,index:n},e,a)}).on("mouseout",function(){clearHoverEffects(a),t.tooltip.style("display","none")})}function setClickEffect(t,e,n=[]){const o=e.nodes(),i=repeatExact(n||[],o.length);e.each(function(t,e){hasClickHandler(i[e])?this.classList.add("clickable"):this.classList.remove("clickable")}).on("click",function(t){const e=o.indexOf(this),n=getClickHandler(i[e]);n&&n.call(this,t)})}function setNearestHoverEffect(t,e,n,o){const i=e.select(`g#${n}`).node();if(!i)return;const r=getPanelBounds(t,n),s=getHoverRecords(o),l=getNearestAnchors(t,s,r);if(0===l.length)return;const c=d3.quadtree().x(t=>t.x).y(t=>t.y).addAll(l),a=new Map(s.map(t=>[t.node,t]));r&&ensureNearestHoverPanel(i,r);const u={axesNode:i,activeRecord:null,hoverConfigs:o,nearestIndex:c,panelBounds:r,recordByNode:a};let f=null,p=null,h=!1;e.on(`mousemove.nearest-${n}`,function(e){if(h=!0,p=e,null!==f)return;const n=e.view?.requestAnimationFrame?e.view.requestAnimationFrame.bind(e.view):t=>t();f=!0;const o=n(()=>{f=null,h&&null!==p&&updateNearestHover(t,p,u)});null!==f&&void 0!==o&&(f=o)}).on(`mouseleave.nearest-${n}`,function(e){h=!1,p=null,null!==f&&e.view?.cancelAnimationFrame&&e.view.cancelAnimationFrame(f),f=null,u.activeRecord=null,clearHoverEffects(o),t.tooltip.style("display","none")})}function updateNearestHover(t,e,n){const o=eventToSvgPoint(t,e);if(!o||n.panelBounds&&!pointInBounds(o,n.panelBounds))return void clearActiveNearestHover(t,n);const i=getDirectHoverRecord(e,n),r=null==i?n.nearestIndex.find(o.x,o.y):null,s=i||r?.record;if(s)return s!==n.activeRecord?(applyHoverRecord(t,s,e,n.hoverConfigs),void(n.activeRecord=s)):void positionTooltip(t,e,s.hoverConfig.showTooltip);clearActiveNearestHover(t,n)}function ensureNearestHoverPanel(t,e){let n=t.querySelector("rect.nearest-hover-panel");n||(n=t.ownerDocument.createElementNS("http://www.w3.org/2000/svg","rect"),n.setAttribute("class","nearest-hover-panel"),t.insertBefore(n,t.firstChild)),n.setAttribute("x",e.x),n.setAttribute("y",e.y),n.setAttribute("width",e.width),n.setAttribute("height",e.height),n.setAttribute("fill","transparent"),n.setAttribute("pointer-events","all")}function clearActiveNearestHover(t,e){null!==e.activeRecord&&(e.activeRecord=null,clearHoverEffects(e.hoverConfigs),t.tooltip.style("display","none"))}function getHoverRecords(t){const e=[];for(const n of t){const t=n.tooltipLabels||[],o=n.tooltipGroups||[];if(0===t.length&&0===o.length)continue;const i=n.plotElements.nodes(),r=normalizeHoverConfig(n,i.length);for(let t=0;t<i.length;t+=1)e.push({hoverConfig:r,index:t,node:i[t]})}return e}function getNearestAnchors(t,e,n=null){const o=[];for(const i of e){const e=getNodeAnchorPoints(t,i.node);for(const t of e)isFinitePoint(t)&&(null===n||pointInBounds(t,n))&&o.push({x:t.x,y:t.y,record:i})}return o}function getDirectHoverRecord(t,e){const n=closestPlotElement(t.target,e.axesNode);return n&&e.recordByNode.get(n)||null}function closestPlotElement(t,e){let n=t;for(;n&&n!==e.parentNode;){if(n.classList?.contains("plot-element")&&e.contains(n))return n;n=n.parentNode}return null}function setZoomEffect(t,e={}){if(!t.node()||"function"!=typeof d3.zoom)return null;let n=t.select('g[id^="figure_"]');if(n.empty()&&(n=t.select("g")),n.empty())return null;const o=e.minScale??1,i=e.maxScale??8,r=d3.zoom().scaleExtent([o,i]).on("zoom",e=>{n.attr("transform",e.transform),t.classed("zoomed",1!==e.transform.k)});return t.call(r),t.classed("zoomable",!0),t.on("dblclick.zoom",null),t.on("dblclick.zoom-reset",()=>{t.transition().duration(200).call(r.transform,d3.zoomIdentity)}),r}class PlotSVGParser{constructor(t,e,n){this.svg=t,this.tooltip=e,this.sanitizer=n??globalThis.DOMPurify,this.tooltip_sanitize_config={USE_PROFILES:{html:!0}},this.path_sample_spacing=12,this.max_path_samples=48}findBars(t,e,n=[]){const o=t.selectAll(`g#${e} g[id^="PolyCollection_"] path`);let i=0;return o.each(function(){d3.select(this).attr("data-group",n[i]),i+=1}),o.attr("class","bar plot-element"),o}findPoints(t,e,n){const o=t.selectAll(`g#${e} g[id^="PathCollection"]`);let i=o.selectAll("use");i.empty()&&(i=o.selectAll("path"));let r=0;return i.each(function(){d3.select(this).attr("data-group",n[r]),r+=1}),i.attr("class","point plot-element"),i}findLines(t,e){const n=t.selectAll(`g#${e} g[id^="line2d"] path`).filter(function(){return!this.closest('g[id^="matplotlib.axis"]')});return n.attr("class","line plot-element"),n}findAreas(t,e){const n=t.selectAll(`g#${e} g[id^="FillBetweenPolyCollection"] path`);return n.attr("class","area plot-element"),n}findPolygons(t,e){const n=t.selectAll(`g#${e} g[id^="PatchCollection_"] path`);return n.attr("class","polygon plot-element"),n}getSvgSummary(t,e){const n=t.selectAll('g[id^="axes_"]').nodes().map(t=>t.id),o=Object.keys(e||{}),i=n.filter(t=>!o.includes(t)),r=[`SVG ${[t.attr("width"),t.attr("height")].filter(Boolean).join(" x ")||"unknown size"};`,`Found ${this._formatCount(n.length,"plot area")} ${this._formatIds(n)}.`];return i.length>0&&r.push(`Will ignore ${this._formatCount(i.length,"unconfigured plot area")} ${this._formatIds(i)}.`),r.join(" ")}getAxesSummary(t,e){return{axesClass:t,plotElements:{points:this._selectionSize(e.points),lines:this._selectionSize(e.lines),bars:this._selectionSize(e.bars),areas:this._selectionSize(e.areas),polygons:this._selectionSize(e.polygons)}}}logParseSummary(t,e){if("undefined"==typeof console)return;"function"==typeof console.groupCollapsed?console.groupCollapsed("[ninejs] parsed chart"):console.log("[ninejs] parsed chart"),console.log(`[ninejs] ${t}`);const n=e.map(t=>({axes:t.axesClass,points:t.plotElements.points,lines:t.plotElements.lines,bars:t.plotElements.bars,areas:t.plotElements.areas,polygons:t.plotElements.polygons}));console.table(n),console.groupEnd()}_selectionSize(t){return t&&"function"==typeof t.size?t.size():0}_formatCount(t,e){return 1===t?`1 ${e}`:`${t} ${e}s`}_formatIds(t){return 0===t.length?"(none)":`(${t.join(", ")})`}setHoverEffect(t,e,n,o,i=!1,r=[]){return setHoverEffect(this,t,e,n,o,i,r)}setClickEffect(t,e=[]){return setClickEffect(this,t,e)}setNearestHoverEffect(t,e,n){return setNearestHoverEffect(this,t,e,n)}setZoomEffect(t,e={}){return setZoomEffect(this,t,e)}}function initPlot(){const t=document.getElementById("plot-container"),e=d3.select("#tooltip"),n=d3.select(t).select("svg"),o=JSON.parse(document.getElementById("plot-data").textContent),i=o.hover_nearest||!1,r=o.reverse_hover||!1,s=o.zoomable||!1,l=o.axes,c=new PlotSVGParser(n,e),a=c.getSvgSummary(n,l),u=[],f={points:(t,e)=>c.findPoints(n,t,e),lines:t=>c.findLines(n,t),bars:(t,e)=>c.findBars(n,t,e),areas:t=>c.findAreas(n,t),polygons:t=>c.findPolygons(n,t)},p=Object.keys(f);for(const t in l){if(!l.hasOwnProperty(t))continue;const e=l[t],o=e.tooltip_labels,s=e.tooltip_groups,a=e.click_handlers||[],h={},g=[];for(const n of p){const i=e[n],l=i?i.tooltip_labels||[]:o,c=i?i.tooltip_groups||[]:s,u=i?i.click_handlers||[]:a,p=f[n](t,c);h[n]=p,g.push({plotElements:p,tooltipLabels:l,tooltipGroups:c,clickHandlers:u,showTooltip:0===l.length?"none":"block",reverseHover:r})}if(u.push(c.getAxesSummary(t,h)),i){for(const t of g)c.setClickEffect(t.plotElements,t.clickHandlers);c.setNearestHoverEffect(n,t,g)}else for(const t of g)c.setHoverEffect(t.plotElements,t.tooltipLabels,t.tooltipGroups,t.showTooltip,t.reverseHover,t.clickHandlers)}s&&c.setZoomEffect(n),c.logParseSummary(a,u)}
function getNodeAnchorPoints(t,e){if(e.classList?.contains("point"))return getBBoxAnchorPoints(t,e,!1);const n=getPathSamplePoints(t,e);return n.length>0?n:getBBoxAnchorPoints(t,e,!0)}function getPathSamplePoints(t,e){if("function"!=typeof e.getTotalLength||"function"!=typeof e.getPointAtLength)return[];let n;try{n=e.getTotalLength()}catch{return[]}if(!Number.isFinite(n)||n<=0)return[];const o=Math.max(1,Math.min(t.nearest_max_samples,Math.ceil(n/t.path_sample_spacing))),i=[];for(let r=0;r<=o;r+=1)try{const s=e.getPointAtLength(n*r/o);i.push(nodePointToSvg(t,e,s.x,s.y))}catch{return i}return i}function getBBoxAnchorPoints(t,e,n){const o=getNodeBBox(t,e);if(!o)return[];const i=o.x+o.width/2,r=o.y+o.height/2,s=[{x:i,y:r}];return n&&s.push({x:o.x,y:o.y},{x:o.x+o.width,y:o.y},{x:o.x,y:o.y+o.height},{x:o.x+o.width,y:o.y+o.height},{x:i,y:o.y},{x:i,y:o.y+o.height},{x:o.x,y:r},{x:o.x+o.width,y:r}),s.map(n=>nodePointToSvg(t,e,n.x,n.y))}function getNodeBBox(t,e){if("function"==typeof e.getBBox)try{const t=e.getBBox();if(isFiniteBBox(t))return t}catch{}return getAttributeBBox(e)}function getAttributeBBox(t){const e=numberAttribute(t,"x"),n=numberAttribute(t,"y"),o=numberAttribute(t,"width"),i=numberAttribute(t,"height");if(Number.isFinite(e)&&Number.isFinite(n)&&Number.isFinite(o)&&Number.isFinite(i))return{x:e,y:n,width:o,height:i};const r=numberAttribute(t,"cx"),s=numberAttribute(t,"cy"),l=numberAttribute(t,"r");return Number.isFinite(r)&&Number.isFinite(s)&&Number.isFinite(l)?{x:r-l,y:s-l,width:2*l,height:2*l}:null}function numberAttribute(t,e){const n=t.getAttribute(e);if(null===n)return NaN;const o=Number(n);return Number.isFinite(o)?o:NaN}function getPanelBounds(t,e){const n=getAxesClipBounds(t,e);if(n)return n;const o=t.svg.select(`g#${e}`).node();return o?getNodeBBox(t,o):null}function getAxesClipBounds(t,e){const n=t.svg.select(`g#${e}`).node(),o=n?.querySelector("[clip-path]"),i=o?.getAttribute("clip-path"),r=getClipPathId(i);if(!r)return null;const s=t.svg.node().ownerDocument.getElementById(r),l=s?.querySelector("rect");return l?getAttributeBBox(l):null}function getClipPathId(t){if(!t)return null;const e=/url\(["']?#([^)"']+)["']?\)/.exec(t);return e?e[1]:null}function eventToSvgPoint(t,e){const n=e.clientX??e.pageX,o=e.clientY??e.pageY;return Number.isFinite(n)&&Number.isFinite(o)?clientPointToSvg(t,n,o):null}function clientPointToSvg(t,e,n){const o=t.svg.node();if(o&&"function"==typeof o.createSVGPoint&&"function"==typeof o.getScreenCTM)try{const t=o.getScreenCTM();if(t){const i=o.createSVGPoint();return i.x=e,i.y=n,i.matrixTransform(t.inverse())}}catch{}return clientPointToSvgFromViewBox(t,e,n)}function clientPointToSvgFromViewBox(t,e,n){const o=t.svg.node();if(!o||"function"!=typeof o.getBoundingClientRect)return{x:e,y:n};const i=o.getBoundingClientRect(),r=getSvgViewBox(o,i);return r&&Number.isFinite(i.width)&&Number.isFinite(i.height)&&0!==i.width&&0!==i.height?{x:r.x+(e-i.left)*r.width/i.width,y:r.y+(n-i.top)*r.height/i.height}:{x:e,y:n}}function getSvgViewBox(t,e){const n=t.viewBox?.baseVal;if(n&&n.width>0&&n.height>0)return n;const o=t.getAttribute("viewBox");if(o){const t=o.trim().split(/[\s,]+/).map(t=>Number(t));if(4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>0&&t[3]>0)return{x:t[0],y:t[1],width:t[2],height:t[3]}}return e.width>0&&e.height>0?{x:0,y:0,width:e.width,height:e.height}:null}function nodePointToSvg(t,e,n,o){const i=t.svg.node();if(i&&"function"==typeof i.createSVGPoint&&"function"==typeof i.getScreenCTM&&"function"==typeof e.getScreenCTM)try{const t=e.getScreenCTM(),r=i.getScreenCTM();if(t&&r){const e=i.createSVGPoint();return e.x=n,e.y=o,e.matrixTransform(t).matrixTransform(r.inverse())}}catch{}if(i&&"function"==typeof i.createSVGPoint&&"function"==typeof i.getCTM&&"function"==typeof e.getCTM)try{const t=e.getCTM(),r=i.getCTM();if(t&&r){const e=i.createSVGPoint();return e.x=n,e.y=o,e.matrixTransform(t).matrixTransform(r.inverse())}}catch{}return{x:n,y:o}}function isFinitePoint(t){return t&&Number.isFinite(t.x)&&Number.isFinite(t.y)}function isFiniteBBox(t){return t&&Number.isFinite(t.x)&&Number.isFinite(t.y)&&Number.isFinite(t.width)&&Number.isFinite(t.height)}function pointInBounds(t,e){return t.x>=e.x&&t.x<=e.x+e.width&&t.y>=e.y&&t.y<=e.y+e.height}function setTooltipContent(t,e){const n=null==e?"":String(e);t.sanitizer&&"function"==typeof t.sanitizer.sanitize?t.tooltip.html(t.sanitizer.sanitize(n,t.tooltip_sanitize_config)):t.tooltip.text(n)}function repeatExact(t,e){return 0===t.length||t.length===e?t:e>t.length&&e%t.length===0?Array.from({length:e},(e,n)=>t[n%t.length]):t}function hasClickHandler(t){return null!=t&&(("number"!=typeof t||!Number.isNaN(t))&&""!==String(t).trim())}function getClickHandler(t){if("function"==typeof t)return t;if(!hasClickHandler(t))return null;const e=globalThis.ninejs?.clickHandlers;if(!e)return null;const n=e[String(t)];return"function"==typeof n?n:null}function normalizeHoverConfig(t,e){const n=repeatExact(t.tooltipLabels||[],e);let o=repeatExact(t.tooltipGroups||[],e);const i=repeatExact(t.clickHandlers||[],e);return 0===o.length&&(n.length>0||i.length>0)&&(o=Array.from({length:e},(t,e)=>e)),{...t,tooltipLabels:n,tooltipGroups:o,clickHandlers:i}}function clearHoverEffects(t){for(const e of t)e.plotElements.classed("not-hovered",!1).classed("hovered",!1)}function positionTooltip(t,e,n){t.tooltip.style("display",n).style("left",e.pageX+"px").style("top",e.pageY+"px")}function applyHoverRecord(t,e,n,o){const i=e.hoverConfig,r=i.tooltipGroups,s=r[e.index];clearHoverEffects(o);const l=i.plotElements.filter((t,e)=>r[e]===s);i.reverseHover?l.classed("not-hovered",!0):(i.plotElements.classed("not-hovered",!0),l.classed("not-hovered",!1).classed("hovered",!0)),positionTooltip(t,n,i.showTooltip),setTooltipContent(t,i.tooltipLabels[e.index])}function setHoverEffect(t,e,n,o,i,r=!1,s=[]){const l=e.nodes(),c=normalizeHoverConfig({plotElements:e,tooltipLabels:n,tooltipGroups:o,showTooltip:i,reverseHover:r,clickHandlers:s},l.length),a=[c];setClickEffect(t,e,c.clickHandlers),e.on("mouseover",function(e){const n=l.indexOf(this);applyHoverRecord(t,{hoverConfig:c,index:n},e,a)}).on("mouseout",function(){clearHoverEffects(a),t.tooltip.style("display","none")})}function setClickEffect(t,e,n=[]){const o=e.nodes(),i=repeatExact(n||[],o.length);e.each(function(t,e){hasClickHandler(i[e])?this.classList.add("clickable"):this.classList.remove("clickable")}).on("click",function(t){const e=o.indexOf(this),n=getClickHandler(i[e]);n&&n.call(this,t)})}function setNearestHoverEffect(t,e,n,o){const i=e.select(`g#${n}`).node();if(!i)return;const r=getPanelBounds(t,n),s=getHoverRecords(o),l=getNearestAnchors(t,s,r);if(0===l.length)return;const c=d3.quadtree().x(t=>t.x).y(t=>t.y).addAll(l),a=new Map(s.map(t=>[t.node,t]));r&&ensureNearestHoverPanel(i,r);const u={axesNode:i,activeRecord:null,hoverConfigs:o,nearestIndex:c,panelBounds:r,recordByNode:a};let f=null,p=null,h=!1;e.on(`mousemove.nearest-${n}`,function(e){if(h=!0,p=e,null!==f)return;const n=e.view?.requestAnimationFrame?e.view.requestAnimationFrame.bind(e.view):t=>t();f=!0;const o=n(()=>{f=null,h&&null!==p&&updateNearestHover(t,p,u)});null!==f&&void 0!==o&&(f=o)}).on(`mouseleave.nearest-${n}`,function(e){h=!1,p=null,null!==f&&e.view?.cancelAnimationFrame&&e.view.cancelAnimationFrame(f),f=null,u.activeRecord=null,clearHoverEffects(o),t.tooltip.style("display","none")})}function updateNearestHover(t,e,n){const o=eventToSvgPoint(t,e);if(!o||n.panelBounds&&!pointInBounds(o,n.panelBounds))return void clearActiveNearestHover(t,n);const i=getDirectHoverRecord(e,n),r=null==i?n.nearestIndex.find(o.x,o.y):null,s=i||r?.record;if(s)return s!==n.activeRecord?(applyHoverRecord(t,s,e,n.hoverConfigs),void(n.activeRecord=s)):void positionTooltip(t,e,s.hoverConfig.showTooltip);clearActiveNearestHover(t,n)}function ensureNearestHoverPanel(t,e){let n=t.querySelector("rect.nearest-hover-panel");n||(n=t.ownerDocument.createElementNS("http://www.w3.org/2000/svg","rect"),n.setAttribute("class","nearest-hover-panel"),t.insertBefore(n,t.firstChild)),n.setAttribute("x",e.x),n.setAttribute("y",e.y),n.setAttribute("width",e.width),n.setAttribute("height",e.height),n.setAttribute("fill","transparent"),n.setAttribute("pointer-events","all")}function clearActiveNearestHover(t,e){null!==e.activeRecord&&(e.activeRecord=null,clearHoverEffects(e.hoverConfigs),t.tooltip.style("display","none"))}function getHoverRecords(t){const e=[];for(const n of t){const t=n.tooltipLabels||[],o=n.tooltipGroups||[];if(0===t.length&&0===o.length)continue;const i=n.plotElements.nodes(),r=normalizeHoverConfig(n,i.length);for(let t=0;t<i.length;t+=1)e.push({hoverConfig:r,index:t,node:i[t]})}return e}function getNearestAnchors(t,e,n=null){const o=[];for(const i of e){const e=getNodeAnchorPoints(t,i.node);for(const t of e)isFinitePoint(t)&&(null===n||pointInBounds(t,n))&&o.push({x:t.x,y:t.y,record:i})}return o}function getDirectHoverRecord(t,e){const n=closestPlotElement(t.target,e.axesNode);return n&&e.recordByNode.get(n)||null}function closestPlotElement(t,e){let n=t;for(;n&&n!==e.parentNode;){if(n.classList?.contains("plot-element")&&e.contains(n))return n;n=n.parentNode}return null}function setZoomEffect(t,e={}){if(!t.node()||"function"!=typeof d3.zoom)return null;let n=t.select('g[id^="figure_"]');if(n.empty()&&(n=t.select("g")),n.empty())return null;const o=e.minScale??1,i=e.maxScale??8,r=d3.zoom().scaleExtent([o,i]).on("zoom",e=>{n.attr("transform",e.transform),t.classed("zoomed",1!==e.transform.k)});return t.call(r),t.classed("zoomable",!0),t.on("dblclick.zoom",null),t.on("dblclick.zoom-reset",()=>{t.transition().duration(200).call(r.transform,d3.zoomIdentity)}),r}class PlotSVGParser{constructor(t,e,n){this.svg=t,this.tooltip=e,this.sanitizer=n??globalThis.DOMPurify,this.tooltip_sanitize_config={USE_PROFILES:{html:!0}},this.path_sample_spacing=12,this.nearest_max_samples=48}findBars(t,e,n=[]){const o=t.selectAll(`g#${e} g[id^="PolyCollection_"] path`);let i=0;return o.each(function(){d3.select(this).attr("data-group",n[i]),i+=1}),o.attr("class","bar plot-element"),o}findPoints(t,e,n){const o=t.selectAll(`g#${e} g[id^="PathCollection"]`);let i=o.selectAll("use");i.empty()&&(i=o.selectAll("path"));let r=0;return i.each(function(){d3.select(this).attr("data-group",n[r]),r+=1}),i.attr("class","point plot-element"),i}findLines(t,e){const n=t.selectAll(`g#${e} g[id^="line2d"] path`).filter(function(){return!this.closest('g[id^="matplotlib.axis"]')});return n.attr("class","line plot-element"),n}findAreas(t,e){const n=t.selectAll(`g#${e} g[id^="FillBetweenPolyCollection"] path`);return n.attr("class","area plot-element"),n}findPolygons(t,e){const n=t.selectAll(`g#${e} g[id^="PatchCollection_"] path`);return n.attr("class","polygon plot-element"),n}getSvgSummary(t,e){const n=t.selectAll('g[id^="axes_"]').nodes().map(t=>t.id),o=Object.keys(e||{}),i=n.filter(t=>!o.includes(t)),r=[`SVG ${[t.attr("width"),t.attr("height")].filter(Boolean).join(" x ")||"unknown size"};`,`Found ${this._formatCount(n.length,"plot area")} ${this._formatIds(n)}.`];return i.length>0&&r.push(`Will ignore ${this._formatCount(i.length,"unconfigured plot area")} ${this._formatIds(i)}.`),r.join(" ")}getAxesSummary(t,e){return{axesClass:t,plotElements:{points:this._selectionSize(e.points),lines:this._selectionSize(e.lines),bars:this._selectionSize(e.bars),areas:this._selectionSize(e.areas),polygons:this._selectionSize(e.polygons)}}}logParseSummary(t,e){if("undefined"==typeof console)return;"function"==typeof console.groupCollapsed?console.groupCollapsed("[ninejs] parsed chart"):console.log("[ninejs] parsed chart"),console.log(`[ninejs] ${t}`);const n=e.map(t=>({axes:t.axesClass,points:t.plotElements.points,lines:t.plotElements.lines,bars:t.plotElements.bars,areas:t.plotElements.areas,polygons:t.plotElements.polygons}));console.table(n),console.groupEnd()}_selectionSize(t){return t&&"function"==typeof t.size?t.size():0}_formatCount(t,e){return 1===t?`1 ${e}`:`${t} ${e}s`}_formatIds(t){return 0===t.length?"(none)":`(${t.join(", ")})`}setHoverEffect(t,e,n,o,i=!1,r=[]){return setHoverEffect(this,t,e,n,o,i,r)}setClickEffect(t,e=[]){return setClickEffect(this,t,e)}setNearestHoverEffect(t,e,n){return setNearestHoverEffect(this,t,e,n)}setZoomEffect(t,e={}){return setZoomEffect(this,t,e)}}function initPlot(){const t=document.getElementById("plot-container"),e=d3.select("#tooltip"),n=d3.select(t).select("svg"),o=JSON.parse(document.getElementById("plot-data").textContent),i=o.hover_nearest||!1,r=o.reverse_hover||!1,s=o.zoomable||!1,l=o.axes,c=new PlotSVGParser(n,e),a=c.getSvgSummary(n,l),u=[],f={points:(t,e)=>c.findPoints(n,t,e),lines:t=>c.findLines(n,t),bars:(t,e)=>c.findBars(n,t,e),areas:t=>c.findAreas(n,t),polygons:t=>c.findPolygons(n,t)},p=Object.keys(f);for(const t in l){if(!l.hasOwnProperty(t))continue;const e=l[t],o=e.tooltip_labels,s=e.tooltip_groups,a=e.click_handlers||[],h={},g=[];for(const n of p){const i=e[n],l=i?i.tooltip_labels||[]:o,c=i?i.tooltip_groups||[]:s,u=i?i.click_handlers||[]:a,p=f[n](t,c);h[n]=p,g.push({plotElements:p,tooltipLabels:l,tooltipGroups:c,clickHandlers:u,showTooltip:0===l.length?"none":"block",reverseHover:r})}if(u.push(c.getAxesSummary(t,h)),i){for(const t of g)c.setClickEffect(t.plotElements,t.clickHandlers);c.setNearestHoverEffect(n,t,g)}else for(const t of g)c.setHoverEffect(t.plotElements,t.tooltipLabels,t.tooltipGroups,t.showTooltip,t.reverseHover,t.clickHandlers)}s&&c.setZoomEffect(n),c.logParseSummary(a,u)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare the embedded runtime hash/signature across iframe outputs to confirm drift.
rg -o 'ninejs-sources-hash: [0-9a-f]+' docs/iframes/*.html
rg -n 'nearest_sample_spacing|path_sample_spacing|zoom_max_scale|minScale' docs/iframes/point.html

Repository: y-sunflower/ninejs

Length of output: 17851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  docs/iframes/point.html
  docs/iframes/hover-nearest.html
  docs/iframes/javascript-hello.html
  docs/iframes/tooltip-html-injection.html
)

echo "== hashes =="
rg -n -o 'ninejs-sources-hash: [0-9a-f]+' "${files[@]}" || true

echo
echo "== key/signature search =="
for f in "${files[@]}"; do
  echo "--- $f ---"
  rg -n 'nearest_sample_spacing|path_sample_spacing|nearest_max_samples|zoom_max_scale|zoom_reset_duration|hoverKeys|minScale|maxScale|duration\(200\)|new PlotSVGParser\(' "$f" || true
  echo
done

Repository: y-sunflower/ninejs

Length of output: 50374


Regenerate point.html with the shared NineJS runtime bundle
This iframe still embeds the older runtime (ninejs-sources-hash differs from the refreshed sibling iframes) and lacks the newer nearest_sample_spacing, nearest_max_samples, zoom_max_scale, zoom_reset_duration, and hover_keys plumbing. If this demo is meant to follow the same plot-data schema, those settings will be ignored here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/iframes/point.html` at line 15, point.html is still using an outdated
embedded NineJS runtime and missing the newer plot-data options, so regenerate
the iframe with the shared runtime bundle. Update the runtime used by
initPlot/PlotSVGParser so it picks up the same schema support as sibling
iframes, including nearest_sample_spacing, nearest_max_samples, zoom_max_scale,
zoom_reset_duration, and hover_keys.

Comment thread ninejs/main.py
Comment on lines +73 to +76
zoom_max_scale: float = 8,
zoom_reset_duration: int = 200,
nearest_sample_spacing: int = 12,
nearest_max_samples: int = 48,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Consider validating the new zoom/nearest-hover parameters.

zoom_max_scale, zoom_reset_duration, nearest_sample_spacing, and nearest_max_samples are forwarded straight into plot_data_json with no bounds checking. A zoom_max_scale <= 1 produces a degenerate d3.zoom().scaleExtent([1, max_scale]) in PlotParserZoom.js, silently locking zoom at scale 1; non-positive nearest_sample_spacing/nearest_max_samples similarly degrade nearest-hover sampling without any error surfaced to the user.

🛡️ Suggested validation
         zoom_max_scale: float = 8,
         zoom_reset_duration: int = 200,
         nearest_sample_spacing: int = 12,
         nearest_max_samples: int = 48,
         **savefig_kws: Any,
     ) -> None:
         """
         Underlying private class that handles most of the work. Practical
         wrapper around `interactive()`.
         """
+        if zoom_max_scale <= 1:
+            raise ValueError("zoom_max_scale must be greater than 1")
+        if nearest_sample_spacing <= 0:
+            raise ValueError("nearest_sample_spacing must be positive")
+        if nearest_max_samples <= 0:
+            raise ValueError("nearest_max_samples must be positive")
         if fig is None:
             fig = plt.gcf()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
zoom_max_scale: float = 8,
zoom_reset_duration: int = 200,
nearest_sample_spacing: int = 12,
nearest_max_samples: int = 48,
zoom_max_scale: float = 8,
zoom_reset_duration: int = 200,
nearest_sample_spacing: int = 12,
nearest_max_samples: int = 48,
**savefig_kws: Any,
) -> None:
"""
Underlying private class that handles most of the work. Practical
wrapper around `interactive()`.
"""
if zoom_max_scale <= 1:
raise ValueError("zoom_max_scale must be greater than 1")
if nearest_sample_spacing <= 0:
raise ValueError("nearest_sample_spacing must be positive")
if nearest_max_samples <= 0:
raise ValueError("nearest_max_samples must be positive")
if fig is None:
fig = plt.gcf()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ninejs/main.py` around lines 73 - 76, Add validation for the new
zoom/nearest-hover parameters in the entrypoint that forwards them into
plot_data_json, since invalid values currently propagate into PlotParserZoom.js
and nearest-hover sampling. In the function that defines zoom_max_scale,
zoom_reset_duration, nearest_sample_spacing, and nearest_max_samples, reject or
clamp zoom_max_scale values at or below 1, and ensure nearest_sample_spacing and
nearest_max_samples are positive before calling plot_data_json; keep the check
close to the parameter parsing/dispatch so users get an immediate error instead
of a broken plot.

@JosephBARBIERDARNAL
JosephBARBIERDARNAL merged commit 2cfc35c into main Jul 7, 2026
22 checks passed
@JosephBARBIERDARNAL
JosephBARBIERDARNAL deleted the cleanups branch July 7, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant