diff --git a/CHANGELOG.md b/CHANGELOG.md index da223dcb..d6e6c198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -142,6 +142,16 @@ in the README). contract without importing the widget stack. ### Changed +- **Density normalization is now uniform-only and CPU-cache-free.** The browser + uploads first-paint, live-update, and worker-rebinned log-u8 sources directly + into R8 textures, then releases transient attachments; cached/home density + windows retain their texture and metadata, not a second CPU grid. + Exposure easing now changes a shader scalar instead of decoding to f32, + requantizing every cell, and calling `texImage2D` on every animation frame. + The shader normalizes and rounds the four source texels before a manual + bilinear blend, preserving the former CPU-requantized LINEAR visual order; + settled frames keep their single hardware-LINEAR lookup, and cache + accounting reports zero retained CPU grid bytes. - **Responsive, author-defeatable browser chrome.** XY's visual defaults now live in a low-priority cascade layer, so Tailwind utilities, ordinary author CSS, and slot styles override them without `!important`. Long legends remain diff --git a/benchmarks/README.md b/benchmarks/README.md index b24e181d..96723ca3 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -104,6 +104,9 @@ These commands match the non-blocking GitHub Actions measurement lane: --json heatmap-4b.json .venv/bin/python benchmarks/bench_interaction.py --sizes 1e4,2.5e5 \ --reps 24 --chromium "$CHROME" --json interaction.json +.venv/bin/python benchmarks/bench_density_normalization.py \ + --points 500000 --frames 240 --chromium "$CHROME" \ + --json density-normalization.json .venv/bin/python benchmarks/bench_transport.py --n 1e6 --reps 15 \ --browser-reps 12 --chromium "$CHROME" --require-browser \ --json transport.json @@ -151,6 +154,14 @@ workflow because they need a real browser, separate processes/virtual environments, or wall-clock timing. They are still measured in CI, but are not reported as CodSpeed simulation benchmarks. +`bench_density_normalization.py` drives the real density exposure clock for a +settled log-u8 surface, calls `gl.finish()` around every measured draw, and +reports frame median/p95/max, `texImage2D` calls, and retained CPU cache bytes. Its +comparison arm runs the removed f32-to-log-u8 grid loop in the same JavaScript +runtime without uploading, attributing the O(cells) CPU work separately. A +valid uniform-only run has zero texture-image calls and zero retained CPU grid +bytes; hardware and SwiftShader reports remain separate. + The suite includes the million-row fixed-width categorical factorizer and the allocation-bounded implicit-row stratified sampler as standalone kernel rows, alongside the complete categorical first-payload row. Together they distinguish diff --git a/benchmarks/bench_density_normalization.py b/benchmarks/bench_density_normalization.py new file mode 100644 index 00000000..0b6e84af --- /dev/null +++ b/benchmarks/bench_density_normalization.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Measure density exposure frames, upload count, and compact cache memory. + +This is a real-Chrome benchmark. It compares the shipped uniform-only draw +path with a JavaScript simulation of the removed CPU f32->log-u8 requantization +loop. The comparison isolates browser work; neither arm includes payload +construction, transport, or density binning. +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import tempfile +from pathlib import Path + +import numpy as np + +import xy +from _browser import chromium_gl_flags, find_chromium # ty: ignore[unresolved-import] +from _cdp import Browser # ty: ignore[unresolved-import] + +ROOT = Path(__file__).resolve().parent.parent + + +def _payload(points: int) -> tuple[dict, bytes]: + rng = np.random.default_rng(166) + left = points // 2 + x = np.r_[rng.normal(-1.0, 0.45, left), rng.normal(1.2, 0.72, points - left)] + y = np.r_[rng.normal(0.75, 0.5, left), rng.normal(-0.65, 0.38, points - left)] + return ( + xy.scatter_chart( + xy.scatter(x=x, y=y, density=True), + width=900, + height=540, + ) + .figure() + .build_payload() + ) + + +def _percentile(values: list[float], fraction: float) -> float | None: + if not values: + return None + ordered = sorted(values) + return ordered[min(len(ordered) - 1, round((len(ordered) - 1) * fraction))] + + +def measure(points: int, frames: int, chrome: str) -> dict[str, object]: + bundle = (ROOT / "python/xy/static/standalone.js").read_text(encoding="utf-8") + spec, blob = _payload(points) + payload = base64.b64encode(blob).decode("ascii") + page = f"""
+""" + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "density-normalization-benchmark.html" + path.write_text(page, encoding="utf-8") + gl = "hardware" if not chromium_gl_flags() else "software" + with Browser(chrome, gl=gl, window=(1100, 700)) as browser: + tab = browser.new_page() + try: + tab.navigate(path.as_uri()) + raw = tab.eval( + f"""(async () => {{ + const view=window.xyDensityBench; + const g=view.gpuTraces.find(trace=>trace.tier==="density"); + const homeDensity=g.density; + const home=view.view0; + const cx=(home.x0+home.x1)/2, cy=(home.y0+home.y1)/2; + const sx=home.x1-home.x0, sy=home.y1-home.y0; + const rebinSeq=++view.seq; + const realUploadDensityGrid=view._uploadDensityGrid; + let uploadedSource=null; + view._uploadDensityGrid=(bytes,w,h)=>{{ + uploadedSource=bytes.slice(); + return realUploadDensityGrid.call(view,bytes,w,h); + }}; + view._requestSampleRebin(g,{{ + x0:cx-sx*0.25,x1:cx+sx*0.25, + y0:cy-sy*0.25,y1:cy+sy*0.25, + }},rebinSeq); + await new Promise((resolve,reject)=>{{ + const deadline=performance.now()+5000; + const poll=()=>{{ + if(g._sampleRebinned&&g.density!==homeDensity) resolve(); + else if(performance.now()>=deadline) reject(new Error("worker rebin timeout")); + else setTimeout(poll,10); + }}; + poll(); + }}); + view._uploadDensityGrid=realUploadDensityGrid; + const density=g.density; + const workerCompact=uploadedSource instanceof Uint8Array + && !("encoded" in density) && !("grid" in density) + && uploadedSource.byteLength===density.w*density.h; + g.sampleOverlay=null; + g.drill=null; + g.prevDensity=null; + g._densitySwitchPrev=null; + g._densitySwitchFadeStart=null; + g._shownDensity=density; + g._densityNormAnim=null; + if(view._raf) cancelAnimationFrame(view._raf); + view._raf=null; + view._drawNow(); view.gl.finish(); + + const encoded=uploadedSource; + const sourceMax=density.max; + const sourceLog=Math.log1p(sourceMax); + const startNorm=Math.expm1(sourceLog/0.45); + const duration=420; + const frameCount={frames}; + const norms=new Float64Array(frameCount); + for(let frame=0;frame[e,[...this._axisRange(e)]]));for(let e of n||this._resetAxisPolicy())r[e]=[...this._axisRange(e,this.view0)];return this._setView({ranges:r},{animate:e,source:t,phase:`end`,interactionId:++this._interactionSeq})},_exportConfig(){let e=this.spec&&this.spec.export;return e&&typeof e==`object`?e:{}},_exportFilename(e){let t=this._exportConfig().filename;return typeof t==`string`&&t?`${t}.${e}`:`${String(this.spec.title||`xy-chart`).trim().toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||`xy-chart`}.${e}`},_downloadExport(e,t){let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,r.style.display=`none`,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(n),0)},_exportSvgMarkup(){this._drawNow?.(),this.gl?.finish?.();let e=this.size.w,t=this.size.h,n=this.root.cloneNode(!0);n.style.width=`${e}px`,n.style.height=`${t}px`,n.style.margin=`0`,n.setAttribute(`xmlns`,`http://www.w3.org/1999/xhtml`);let r=getComputedStyle(this.root),i=[`color`,`font-family`,`font-size`,`font-style`,`font-weight`,`letter-spacing`,`line-height`],a=[`--chart-bg`,`--chart-text`,`--chart-grid`,`--chart-axis`,`--chart-tooltip-bg`,`--chart-tooltip-text`,`--chart-legend-bg`,`--chart-badge-bg`,`--chart-badge-text`,`--chart-modebar-bg`,`--chart-modebar-active`,`--chart-selection`,`--chart-selection-fill`,`--chart-zoom-selection`,`--chart-zoom-selection-fill`,`--chart-crosshair`,`--chart-annotation-text`,`--chart-cursor`,`--chart-cursor-pan`];for(let e=0;e [e,[...this._axisRange(e)]]));for(let e of n||this._resetAxisPolicy())r[e]=[...this._axisRange(e,this.view0)];return this._setView({ranges:r},{animate:e,source:t,phase:`end`,interactionId:++this._interactionSeq})},_exportConfig(){let e=this.spec&&this.spec.export;return e&&typeof e==`object`?e:{}},_exportFilename(e){let t=this._exportConfig().filename;return typeof t==`string`&&t?`${t}.${e}`:`${String(this.spec.title||`xy-chart`).trim().toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||`xy-chart`}.${e}`},_downloadExport(e,t){let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,r.style.display=`none`,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(n),0)},_exportSvgMarkup(){this._drawNow?.(),this.gl?.finish?.();let e=this.size.w,t=this.size.h,n=this.root.cloneNode(!0);n.style.width=`${e}px`,n.style.height=`${t}px`,n.style.margin=`0`,n.setAttribute(`xmlns`,`http://www.w3.org/1999/xhtml`);let r=getComputedStyle(this.root),i=[`color`,`font-family`,`font-size`,`font-style`,`font-weight`,`letter-spacing`,`line-height`],a=[`--chart-bg`,`--chart-text`,`--chart-grid`,`--chart-axis`,`--chart-tooltip-bg`,`--chart-tooltip-text`,`--chart-legend-bg`,`--chart-badge-bg`,`--chart-badge-text`,`--chart-modebar-bg`,`--chart-modebar-active`,`--chart-selection`,`--chart-selection-fill`,`--chart-zoom-selection`,`--chart-zoom-selection-fill`,`--chart-crosshair`,`--chart-annotation-text`,`--chart-cursor`,`--chart-cursor-pan`];for(let e=0;e [e,[...this._axisRange(e)]]));for(let e of n||this._resetAxisPolicy())r[e]=[...this._axisRange(e,this.view0)];return this._setView({ranges:r},{animate:e,source:t,phase:`end`,interactionId:++this._interactionSeq})},_exportConfig(){let e=this.spec&&this.spec.export;return e&&typeof e==`object`?e:{}},_exportFilename(e){let t=this._exportConfig().filename;return typeof t==`string`&&t?`${t}.${e}`:`${String(this.spec.title||`xy-chart`).trim().toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||`xy-chart`}.${e}`},_downloadExport(e,t){let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,r.style.display=`none`,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(n),0)},_exportSvgMarkup(){this._drawNow?.(),this.gl?.finish?.();let e=this.size.w,t=this.size.h,n=this.root.cloneNode(!0);n.style.width=`${e}px`,n.style.height=`${t}px`,n.style.margin=`0`,n.setAttribute(`xmlns`,`http://www.w3.org/1999/xhtml`);let r=getComputedStyle(this.root),i=[`color`,`font-family`,`font-size`,`font-style`,`font-weight`,`letter-spacing`,`line-height`],a=[`--chart-bg`,`--chart-text`,`--chart-grid`,`--chart-axis`,`--chart-tooltip-bg`,`--chart-tooltip-text`,`--chart-legend-bg`,`--chart-badge-bg`,`--chart-badge-text`,`--chart-modebar-bg`,`--chart-modebar-active`,`--chart-selection`,`--chart-selection-fill`,`--chart-zoom-selection`,`--chart-zoom-selection-fill`,`--chart-crosshair`,`--chart-annotation-text`,`--chart-cursor`,`--chart-cursor-pan`];for(let e=0;e[e,[...this._axisRange(e)]]));p[t]=[this._axisValue(n,u?f:d),this._axisValue(n,u?d:f)],this._setView({ranges:p},{animate:!0,anchors:{[t]:.5},source:`box_zoom`,phase:`end`,interactionId:a.interactionId})}}};this._listen(e,`pointerup`,o),this._listen(e,`pointercancel`,o)},_seriesColorCss(e){let t=e&&e.color;if(!Array.isArray(t)||t.length<3)return null;let n=e=>Math.max(0,Math.min(255,Math.round(Number(e)*255)));return`rgb(${n(t[0])}, ${n(t[1])}, ${n(t[2])})`},_hoverPayload(e,t,n,r,i=!1){let a=this.root.getBoundingClientRect(),o=this.canvas.getBoundingClientRect(),s=Math.max(0,Math.min(o.width,n-o.left)),c=Math.max(0,Math.min(o.height,r-o.top)),l={};for(let e of this._axisIds()){let t=this._axisDim(e),[n,r]=this._dataFromCanvas(s,c,t===`x`?e:`x`,t===`y`?e:`y`);l[e]=t===`x`?n:r}let u=t&&t.g,d=e?[{trace:u&&u.trace&&u.trace.name||e.trace,index:e.index,row:e,x_axis:u&&u.xAxis||`x`,y_axis:u&&u.yAxis||`y`,color:this._seriesColorCss(u)}]:[],f={active:!0,cursor:{px:[n-a.left,r-a.top],data:l},points:d};return i&&(f.exact=!0),f},setCustomTooltip(e){if(this.tooltip){if(!e){this._customTooltip=null,delete this.tooltip.dataset.xyCustomTooltip,this.tooltip.style.background=``,this.tooltip.style.border=``,this.tooltip.style.padding=``,this.tooltip.replaceChildren(),this.tooltip.style.display=`none`;return}this._customTooltip=e,this.tooltip.dataset.xyCustomTooltip=``,this.tooltip.style.background=`transparent`,this.tooltip.style.border=`none`,this.tooltip.style.padding=`0`,e.style.display=``,this.tooltip.replaceChildren(e)}}});function yt(e,t){if(e.buffer_layout===`split`){if(!Array.isArray(t))throw Error(`xy: spec says buffer_layout=split but the transport delivered one buffer`);return t.map(o)}if(Array.isArray(t))throw Error(`xy: transport delivered a buffer list but the spec is not split-layout`);return o(t)}function bt({model:e,el:t}){let n=e.get(`spec`),r=new Q(t,n,yt(n,e.get(`buffers`)),{send:t=>e.send(t),wantsViewChange:()=>n.interaction?._transport_view_change===!0,onMessage:t=>{let n=(e,n)=>t(e,n);return e.on(`msg:custom`,n),()=>e.off?.(`msg:custom`,n)}});return()=>r.destroy()}function xt(e,t,n){let r=o(n),i=new Q(e,t,r,null),a=e=>i._columnView(r,t.columns[e]);for(let e of i.gpuTraces)J(e.trace.kind).retainCpu&&e.tier!==`density`&&(e._cpu={x:a(e.trace.x),y:a(e.trace.y),xMeta:e.xMeta,yMeta:e.yMeta},e.trace.color&&Number.isInteger(e.trace.color.buf)&&(e._cpu.color=a(e.trace.color.buf)),e.trace.size&&Number.isInteger(e.trace.size.buf)&&(e._cpu.size=a(e.trace.size.buf)));return i}var St={render:bt,decodeFrame:d};export{Q as ChartView,q as MARK_KINDS,d as decodeFrame,St as default,J as markOf,bt as render,xt as renderStandalone};
\ No newline at end of file
+`;function _t(){try{let e=URL.createObjectURL(new Blob([gt],{type:`application/javascript`})),t=new Worker(e);return t._fcUrl=e,t}catch{return null}}Object.assign(Z.prototype,{_scheduleViewRequest(e=this.view,t={}){if(this._destroyed||this._glLost)return;if(!this.comm){this._scheduleSampleRebin(e,t);return}let n=this.spec.traces.some(e=>e.tier===`decimated`),r=this.gpuTraces.some(e=>e.tier===`density`);if(!n&&!r)return;let i=t.seq??++this.seq,a=this._copyView(e),o=Math.round(this.plot.w),s=Math.round(this.plot.h);if(r){let e=this._now();for(let t of this.gpuTraces)t.tier===`density`&&(t._lodPendingView=a,t._lodPendingSeq=i,t._lodPendingAt=e)}let c=t.delay??120;if(t.maxWait!==void 0&&t.maxWait!==null){let e=this._now();(this._viewRequestBurstStart===void 0||this._viewRequestBurstStart===null)&&(this._viewRequestBurstStart=e);let n=t.maxWait-(e-this._viewRequestBurstStart);c=n<=0?0:Math.min(c,n)}else this._viewRequestBurstStart=null;clearTimeout(this._viewTimer);let l=()=>{if(!this._destroyed&&(this._viewRequestBurstStart=null,i===this.seq&&(n&&this.comm.send({type:`view`,seq:i,x0:Math.min(a.x0,a.x1),x1:Math.max(a.x0,a.x1),px:o}),r)))for(let e of this.gpuTraces){if(e.tier!==`density`)continue;let[t,n]=this._axisRange(e.xAxis,a),[r,c]=this._axisRange(e.yAxis,a);this.comm.send({type:`density_view`,seq:i,trace:e.trace.id,x0:Math.min(t,n),x1:Math.max(t,n),y0:Math.min(r,c),y1:Math.max(r,c),w:o,h:s})}};return c<=0?l():this._viewTimer=setTimeout(l,c),i},_scheduleSampleRebin(e=this.view,t={}){if(this._destroyed||this._glLost||this._sampleRebinDisabled)return;let n=(this.gpuTraces||[]).filter(e=>e.tier===`density`&&e.sampleOverlay&&e.sampleOverlay._cpu);if(!n.length)return;let r=t.seq??++this.seq,i=this._copyView(e);clearTimeout(this._rebinTimer),this._rebinTimer=setTimeout(()=>{if(!(this._destroyed||r!==this.seq))for(let e of n)this._requestSampleRebin(e,i,r)},t.delay??120)},_requestSampleRebin(e,t,n){e._homeDensity||=e.density;let[r,i]=this._axisRange(e.xAxis,t),[a,o]=this._axisRange(e.yAxis,t),[s,c]=this._axisRange(e.xAxis,this.view0),[l,u]=this._axisRange(e.yAxis,this.view0),d=Math.max(Math.abs(c-s),1e-300),f=Math.max(Math.abs(u-l),1e-300),p=Math.abs(i-r),m=Math.abs(o-a);if(p>=d*.999999&&m>=f*.999999){e.density!==e._homeDensity&&this._applySampleRebinGrid(e,e._homeDensity,!1);return}if(!this._sampleRebinDisabled){if(!this._rebinWorker){if(this._rebinWorker=_t(),!this._rebinWorker){this._sampleRebinDisabled=!0;return}this._rebinWorker.onmessage=e=>this._onRebinResult(e.data),this._rebinInit=new Set}if(!this._rebinInit.has(e.trace.id)){let t=e.sampleOverlay._cpu,n=Math.min(t.x.length,t.y.length),r=new Float64Array(n),i=new Float64Array(n);for(let e=0;e[e,[...this._axisRange(e)]]));p[t]=[this._axisValue(n,u?f:d),this._axisValue(n,u?d:f)],this._setView({ranges:p},{animate:!0,anchors:{[t]:.5},source:`box_zoom`,phase:`end`,interactionId:a.interactionId})}}};this._listen(e,`pointerup`,o),this._listen(e,`pointercancel`,o)},_seriesColorCss(e){let t=e&&e.color;if(!Array.isArray(t)||t.length<3)return null;let n=e=>Math.max(0,Math.min(255,Math.round(Number(e)*255)));return`rgb(${n(t[0])}, ${n(t[1])}, ${n(t[2])})`},_hoverPayload(e,t,n,r,i=!1){let a=this.root.getBoundingClientRect(),o=this.canvas.getBoundingClientRect(),s=Math.max(0,Math.min(o.width,n-o.left)),c=Math.max(0,Math.min(o.height,r-o.top)),l={};for(let e of this._axisIds()){let t=this._axisDim(e),[n,r]=this._dataFromCanvas(s,c,t===`x`?e:`x`,t===`y`?e:`y`);l[e]=t===`x`?n:r}let u=t&&t.g,d=e?[{trace:u&&u.trace&&u.trace.name||e.trace,index:e.index,row:e,x_axis:u&&u.xAxis||`x`,y_axis:u&&u.yAxis||`y`,color:this._seriesColorCss(u)}]:[],f={active:!0,cursor:{px:[n-a.left,r-a.top],data:l},points:d};return i&&(f.exact=!0),f},setCustomTooltip(e){if(this.tooltip){if(!e){this._customTooltip=null,delete this.tooltip.dataset.xyCustomTooltip,this.tooltip.style.background=``,this.tooltip.style.border=``,this.tooltip.style.padding=``,this.tooltip.replaceChildren(),this.tooltip.style.display=`none`;return}this._customTooltip=e,this.tooltip.dataset.xyCustomTooltip=``,this.tooltip.style.background=`transparent`,this.tooltip.style.border=`none`,this.tooltip.style.padding=`0`,e.style.display=``,this.tooltip.replaceChildren(e)}}});function St(e,t){if(e.buffer_layout===`split`){if(!Array.isArray(t))throw Error(`xy: spec says buffer_layout=split but the transport delivered one buffer`);return t.map(o)}if(Array.isArray(t))throw Error(`xy: transport delivered a buffer list but the spec is not split-layout`);return o(t)}function $({model:e,el:t}){let n=e.get(`spec`),r=new Z(t,n,St(n,e.get(`buffers`)),{send:t=>e.send(t),wantsViewChange:()=>n.interaction?._transport_view_change===!0,onMessage:t=>{let n=(e,n)=>t(e,n);return e.on(`msg:custom`,n),()=>e.off?.(`msg:custom`,n)}});return()=>r.destroy()}function Ct(e,t,n){let r=o(n),i=new Z(e,t,r,null),a=e=>i._columnView(r,t.columns[e]);for(let e of i.gpuTraces)q(e.trace.kind).retainCpu&&e.tier!==`density`&&(e._cpu={x:a(e.trace.x),y:a(e.trace.y),xMeta:e.xMeta,yMeta:e.yMeta},e.trace.color&&Number.isInteger(e.trace.color.buf)&&(e._cpu.color=a(e.trace.color.buf)),e.trace.size&&Number.isInteger(e.trace.size.buf)&&(e._cpu.size=a(e.trace.size.buf)));return i}var wt={render:$,decodeFrame:d};export{Z as ChartView,K as MARK_KINDS,d as decodeFrame,wt as default,q as markOf,$ as render,Ct as renderStandalone};
\ No newline at end of file
diff --git a/python/xy/static/standalone.js b/python/xy/static/standalone.js
index 07a0f8f2..ee9decbb 100644
--- a/python/xy/static/standalone.js
+++ b/python/xy/static/standalone.js
@@ -51,8 +51,8 @@ var xy=(function(e){Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toS
carries higher specificity; it stays outside the base layer because it
overrides host CSS rather than providing an overridable default. */
.cell-output-ipywidget-background:has(.xy[data-xy-own-bg]){background:transparent!important}
-`;function C(e){let t=e&&e.getRootNode?e.getRootNode():document,n=typeof ShadowRoot<`u`&&t instanceof ShadowRoot;!n&&!(t instanceof Document)&&(t=document);let r=n?t:t.head||document.head||t.documentElement;if(!r||!r.querySelector||r.querySelector(`style[data-xy-chrome]`))return;let i=document.createElement(`style`);i.setAttribute(`data-xy-chrome`,``),i.textContent=S,r.appendChild(i)}function w(e,t,n=[.5,.5,.5,1]){let r=y(e,t,n);return x(Array.isArray(r)&&r.length>=4&&r.every(Number.isFinite)?r:n)}function T(e){if(e=Math.abs(e),!Number.isFinite(e)||e<=0)return 1;let t=10**Math.floor(Math.log10(e));for(let n of[1,2,2.5,5,10])if(e<=n*t*1.000000000001)return n*t;return 10*t}function E(e,t,n=6){if(!Number.isFinite(e)||!Number.isFinite(t))return{ticks:[],step:1};let r=Math.min(e,t),i=Math.max(e,t);if(r===i)return{ticks:[r],step:1};let a=T((i-r)/n),o=Math.ceil(r/a)*a,s=[];for(let e=o;e<=i+a*1e-9&&s.length<200;e+=a)s.push(Math.abs(e)=r*.999999999999&&o<=i*1.000000000001&&(c.push(o),n===1&&(e-a)%u===0&&l.push(o)),c.length>=200)break}}return{ticks:c,labels:l.length?l:c,step:1,log:!0}}function O(e,t,n,r=6){if(!n||!n.length)return{ticks:[],step:1};let i=Math.max(0,Math.ceil(Math.min(e,t))),a=Math.min(n.length-1,Math.floor(Math.max(e,t)));if(a14*k.d)return ee(r,i,a);let o=A[A.length-1];for(let e of A)if(e>=a){o=e;break}let s=Math.ceil(r/o)*o,c=[];for(let e=s;e<=i&&c.length<200;e+=o)c.push(e);return{ticks:c,step:o}}function ee(e,t,n){let r=n/(30*k.d),i=[1,2,3,6,12,24,60,120],a=i[i.length-1];for(let e of i)if(e>=r){a=e;break}let o=new Date(e),s=o.getUTCFullYear(),c=o.getUTCMonth();c=Math.ceil(c/a)*a;let l=[];for(;;){let n=Date.UTC(s+Math.floor(c/12),c%12,1);if(n>t||(n>=e&&l.push(n),c+=a,l.length>1e3))break}return{ticks:l,step:a*30*k.d}}function te(e,t){let n=new Date(e),r=(e,t=2)=>String(e).padStart(t,`0`);return t>=28*k.d?n.getUTCMonth()===0?String(n.getUTCFullYear()):`${n.toLocaleString(`en`,{month:`short`,timeZone:`UTC`})} ${n.getUTCFullYear()}`:t>=k.d?`${n.toLocaleString(`en`,{month:`short`,timeZone:`UTC`})} ${r(n.getUTCDate())}`:t>=k.m?`${r(n.getUTCHours())}:${r(n.getUTCMinutes())}`:t>=k.s?`${r(n.getUTCHours())}:${r(n.getUTCMinutes())}:${r(n.getUTCSeconds())}`:`${r(n.getUTCMinutes())}:${r(n.getUTCSeconds())}.${r(n.getUTCMilliseconds(),3)}`}function M(e,t){let n=Math.abs(e);if(n>=1e6||n!==0&&n<1e-4)return e.toExponential(1).replace(`e+`,`e`);let r=t?Math.max(0,Math.ceil(-Math.log10(Math.abs(t)))):0;for(;r<8&&Math.abs(Number(t.toFixed(r))-t)>Math.abs(t)/1e3;)r++;return e.toFixed(Math.min(r,8))}function ne(e,t=6){let n=Number(e);if(!Number.isFinite(n))return String(e);if(n===0)return Object.is(n,-0)?`-0`:`0`;let[r,i]=n.toExponential(t-1).split(`e`),a=Number(i);if(a<-4||a>=t)return r=r.replace(/0+$/,``).replace(/\.$/,``),`${r}e${a>=0?`+`:`-`}${String(Math.abs(a)).padStart(2,`0`)}`;let o=Math.max(0,t-a-1),s=Number(n.toPrecision(t)).toFixed(o);return s.includes(`.`)&&(s=s.replace(/0+$/,``).replace(/\.$/,``)),s}function re(e,t){let n=Math.round(e);return n>=0&&n