From 9e9bde33b83ff3f16837dd0b7f122af402804934 Mon Sep 17 00:00:00 2001 From: noisedestroyers Date: Sat, 11 Apr 2026 08:21:41 -0400 Subject: [PATCH] p3 improvements --- .coverage | Bin 69632 -> 69632 bytes docs/STATUS.md | 99 ++++++++++-- pyproject.toml | 2 + .../plot/__pycache__/engine.cpython-312.pyc | Bin 9145 -> 10702 bytes src/impakt/plot/engine.py | 103 +++++++++---- src/impakt/web/callbacks/plot_callbacks.py | 40 ++++- src/impakt/web/components/channel_grid.py | 51 ++++++ uv.lock | 145 ++++++++++++++++++ 8 files changed, 397 insertions(+), 43 deletions(-) diff --git a/.coverage b/.coverage index e1c85362a65621119c610714ac75b02af424cccd..aff8561df8cdcd3396d2c6342ef1ab626dc5a1e9 100644 GIT binary patch delta 840 zcmW-de{2(F7{{Nx9^Ku&clUNHtg>}$%VsUbl@h01@Jeg4fZ-1`b3rpE8b~0RFsBK_ zq;Yra$cD15(KD#v(!-cQ_>nXl1c-wc84(0+_z_&3ge{q2QDF^agQM%tcfNnT&+~pi z&+~nsJet9129vgHY9nA1=kW+Wjl1zW9Kj*%#+)=GjY;RE!_qfWpA?eHC0?S$$Kq8n zBYrP_E5^l-#dk$ntPm-|6wV7j2;T|&g`Gl^utBI3Tmpk8Q4S5GL1dt>&~CH^y@l4I zAX%8V9|s9(;j8LRm4{B(a}Py2H>R@0LhW)3XUfBf7R z49LWwQKq$rg|Dx80KlTiwQ#cd{}7wYm@0tg(y9GFsb1-j^;mVjNSXrGA6)DFl>sQJ zcaqAz)hmd&wu(M!T9#GA!nh2tbQvYy_^Lsm^L_c@v?W+?Taex3xt*}eP$x3pJahx zqIIA+dwaL9VPV`&`i|bPjb>~0gT0E{P}&4_U`$OBkJ!G;aHe(pc0aW>eOx-as8hU@ z$96BVkmzQYhs3a-e);gkdNF%pLPbpBayxvY;$;%&ED@1)h@(w}EjBCK?}NmZcs@4+3ZLT)1C z1z`2ozV$vTI{?sQ-Dh2dm-&vFRG^$>jb7T=#erS3ees%;HN=J8j@$Hq`Zk9>vuBga z>@)26?TMZFcVntY;is~x==_D0s!31Y7Sg>l(QJ00YIvsQ(<_lkz24rx(JrqrZQ4Hl zm;Oq&!KRH_cqP^!^m{5|wn69CL5p&5V6vv-?+qAv+x%at1J3v>USg24Lu%WEDLiw;!vt$MrW(aEHTsC!?h#$$F z^FQYZezo9N2Q&iT47fNaaTDAS*Tpq(wOl!8;0W{tO{1UCG4uuc6j=~LBpYWRu@~79 z*3W*$e$2M9+t_M0m%+>ubAkDW>1B>EyO}MFlPP8N5X0N>GCTuM!(Qlt`(ZPD1KMB_ zg<(Q-@D`M-P@hp+;AS z;uvGI4$Rho>O+35sr{d)4i^CSEUUsTM$$iIe0KHpPU5ni)EP=woSr9Nc)7t=l94$9 z>dCOX&{(2$H?DSXMITkodh7BSdEx-*8w;6=XYxje3Ef%+XRS)ap~12;Zgsczr()L* z7QVW2?pCL`x2IB-pvuJKJ$C-J%qH;U#}o7O%47A1L*Q)dd-pBGuD~gD@~uvGT;en) z#RHKoIR#3tw2&K4%sEqmTR*4vx#UB?<1uK=SHQ+%cS}n$hqo^$q2R4Ow>PJkh^(OaCJu3;~acI+uk_Yfxuj7)-t6nP)APYmNBS2ZFQBFCOrU4c5e)Y47+KujajCZBndtIt zF0EM?R|$RJ8l|t7&SI>|Wt3$vA@XEMJ3V=0Uxfo)RS99tRaA(Jo`N+&)qsB2048#& z6&ZB*I^w`obRJ78a5%Tx{4iZ0mV3*sb*I9CGmY)Gln>a3={q%e0e_$emGk^NpNy!b t=Vn|ESmX)LJ-wMNc6pn!cle@syZ<}uY+3`PWP}Mzx7|_0pybUE{sjdVTaf?& diff --git a/docs/STATUS.md b/docs/STATUS.md index 91c0ca1..7707437 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,10 +1,11 @@ # Impakt -- Project Status -**Date:** 2026-04-10 +**Date:** 2026-04-11 **Version:** 0.1.0 -**Tests:** 181 passing -**Source:** ~11,000 lines (py+js+css+html) | ~2,150 lines tests -**Tooling:** uv (Python 3.12.12), hatchling build backend +**Tests:** 240 passing (69.7% coverage) +**Source:** ~10,300 lines (py) | ~2,700 lines tests +**Quality:** 78.3/100 (Grade B) -- see `docs/QA-*.md` +**Tooling:** uv (Python 3.12.12), hatchling build backend, ruff, mypy strict --- @@ -13,7 +14,7 @@ ```bash cd /Users/noise/Code/impakt uv sync --dev # install all dependencies -uv run pytest tests/ # run all 181 tests +uv run pytest tests/ # run all 240 tests (with coverage) uv run impakt info tests/mme_data/3239 # show test metadata uv run impakt serve tests/mme_data/3239 # launch web UI on :8050 ``` @@ -211,13 +212,59 @@ Every selected channel has a stable color index (position in selection order). T --- -## Next Steps (Priority 3) +## Roadmap -1. **Annotations** -- text on plots, measurement lines, highlight regions -2. **Comparison mode** -- side-by-side tests, delta plots, synced cursors -3. **Report builder** -- drag-and-drop report composer, PDF preview -4. **Keyboard shortcuts** -- Ctrl+O, Ctrl+S, 1-9 pane switch, F fullscreen, R reset zoom -5. **Consider Python/WASM frontend** -- NiceGUI or Solara for pure-Python UI (no JS) +Informed by competitive landscape survey (`research/landscape.md`). No open-source web-based tool covers this domain end-to-end. See `BRAINSTORM.md` for full feature ideas with priority tiers. + +### Priority 3 — Performance & Rendering + +Goal: handle large datasets (500+ channels, 100kHz sample rates) without lag. + +1. **plotly-resampler integration** -- Drop-in `FigureResampler` wrapper for Plotly figures. Handles 110M+ points via LTTB downsampling. Works natively with Dash. Repo cloned at `research/repos/plotly-resampler/`. This is the single highest-impact performance improvement. +2. **Synchronized zoom/pan** -- When plotting multiple subplots, zoom/pan syncs across all panes sharing an X axis. Most-requested feature in crash test visualization. Implement via shared `xaxis` config or callback-based range sync. +3. **Lazy channel loading** -- Load `.dat` files on first access, not at `Session.open()`. Load headers eagerly, data lazily. Keeps startup fast for tests with 500+ channels. +4. **Channel sparklines** -- Tiny inline sparklines in the channel grid sidebar. Engineers visually scan 100+ channels before selecting. A 60px-wide sparkline column is transformative for signal browsing. + +### Priority 4 — Data Format Expansion + +Goal: read the world's crash test data, not just ISO MME. + +5. **UDS reader plugin** -- NHTSA's proprietary binary format. Required to access the largest public crash test database. The NHTSA-Tools Fortran source (`research/repos/NHTSA-Tools/`) documents the UDS spec. +6. **ASAM MDF reader plugin** -- Standard for ECU/CAN bus measurement data. Many labs record vehicle bus data alongside crash instrumentation. asammdf (`research/repos/asammdf/`) is a mature library — add as optional dependency. +7. **Flexible CSV reader** -- Column mapping, delimiter detection, header conventions. Engineers frequently receive data as CSV exports from other tools. + +### Priority 5 — Comparison & Reporting + +Goal: make multi-test comparison and deliverable generation effortless. + +8. **Quick comparison mode** -- Two tests side-by-side with synchronized cursors. One-click "compare" button. Color-by-test with channel differentiation via line dash. +9. **Multi-page PDF reports** -- Combine plots + injury summary + protocol rating into a single PDF with table of contents. Currently each report type is standalone. +10. **Excel export** -- Criteria results and cursor values to .xlsx. Engineers live in spreadsheets. +11. **Static HTML export** -- Bundle data + Plotly.js into a self-contained HTML file. Opens in any browser without a Python server. Learned from FalCon's CustomerView distribution model. + +### Priority 6 — Video & Advanced Analysis + +Goal: close the biggest remaining gap vs. commercial tools. + +12. **Video synchronization** -- Link high-speed camera footage with channel data. Scrubbing video moves the time cursor; moving the cursor seeks the video. Every major commercial competitor (measX, DIAdem, Kistler, FalCon) has this. Foxglove and Rerun demonstrate web-native approaches. +13. **Frequency spectrum viewer** -- FFT / PSD alongside time-domain plots. Diagnose noise, verify CFC filter behavior. +14. **Integration / differentiation transforms** -- Acceleration -> velocity -> displacement with cumulative unit tracking. +15. **Data quality dashboard** -- Automated polarity check, sensor sanity, missing channel detection, completeness scoring. No commercial competitor is strong here — opportunity to differentiate. + +### Priority 7 — Simulation Correlation & Ecosystem + +Goal: bridge the gap between physical test and CAE simulation. + +16. **LS-DYNA data import** via lasso-python (`research/repos/lasso-python/`). Enables test-vs-simulation overlay — a premium feature in Altair HyperGraph and Siemens Simcenter. +17. **ISO/TS 18571 CORA correlation** -- Quantitative rating of test-vs-simulation agreement. Standard metric for model validation. +18. **Additional injury criteria** -- BrIC, DAMAGE, TTI, pedestrian criteria, OLC. Required for broader Euro NCAP coverage. +19. **Additional NCAP programs** -- J-NCAP, C-NCAP, K-NCAP, ANCAP, Latin NCAP as protocol plugins. +20. **Jupyter integration** -- `_repr_html_` on Session, Channel, ProtocolResult for rich notebook output. + +### Validation (ongoing) + +- **Cross-validate CFC filter** against PyAvia's J211_2pole (`research/repos/pyavia/`) and NHTSA-Tools' BwFilt. PyAvia's author notes that scipy's generic `sosfiltfilt` may differ from the SAE J211 Appendix C digital Butterworth algorithm for CFC 60 and 180. +- **Cross-validate injury criteria** against NHTSA-Tools Fortran reference implementations, pyisomme, and EPFL crash-tests-service-robots. Four independent codebases available in `research/repos/`. --- @@ -238,7 +285,8 @@ Every selected channel has a stable color index (position in selection order). T Core: numpy, scipy, plotly, dash, dash-bootstrap-components, pandas, pyyaml, jinja2, weasyprint, pydantic Dev: pytest, pytest-cov, ruff, mypy -Optional: nptdms (for future TDMS reader plugin) +Optional: nptdms (TDMS reader plugin) +Planned: plotly-resampler (P3), asammdf (P4), openpyxl (P5), lasso-python (P7) --- @@ -249,4 +297,29 @@ Optional: nptdms (for future TDMS reader plugin) 3. **DataTable deprecation warning** -- Dash recommends migrating to dash-ag-grid. 4. **Cursor poll interval (80ms)** -- slight latency in cursor grid updates. 5. **Chest deflection auto-detect** skips DS channels with peak > 150mm to avoid steering column displacement. -6. **Dead files** -- `cursors.py` and `inspector.py` were replaced by `channel_values.py` and deleted; `inspector_callbacks.py` deleted. If old `.pyc` files cause issues, run `find src -name __pycache__ -type d | xargs rm -rf`. +6. **CFC filter implementation** uses scipy `sosfiltfilt` which may diverge from SAE J211 Appendix C for CFC 60/180. Needs cross-validation against PyAvia and NHTSA-Tools reference implementations. + +--- + +## Quality Assurance + +Automated QA scoring is configured: +- **Scoring agent:** `.claude/agents/quality-scorer.md` -- collects metrics, applies rubrics, writes report +- **Improvement agent:** `.claude/agents/qa-improver.md` -- reads QA report, auto-fixes mechanical issues +- **Methodology:** `docs/QA-INSTRUCTIONS.md` -- reproducible 8-dimension rubric +- **Reports:** `docs/QA-*.md` -- timestamped scorecards with deltas +- **Scripts:** `scripts/qa-score.sh` (score only), `scripts/qa-improve.sh` (score -> fix -> re-score) + +--- + +## Competitive Landscape + +Full survey at `research/landscape.md` with 11 cloned open-source repos in `research/repos/`. + +**Key finding:** No existing tool combines open-source + web-based + ISO-MME + CFC + injury criteria + protocol scoring + templates + reports. Commercial tools (measX X-Crash, NI DIAdem, Kistler) do this but are expensive and Windows-only. Impakt occupies a genuinely unserved niche. + +**Most actionable libraries:** +- plotly-resampler (performance) -- `research/repos/plotly-resampler/` +- asammdf (MDF format) -- `research/repos/asammdf/` +- lasso-python (LS-DYNA) -- `research/repos/lasso-python/` +- PyAvia, NHTSA-Tools (validation) -- `research/repos/pyavia/`, `research/repos/NHTSA-Tools/` diff --git a/pyproject.toml b/pyproject.toml index c8bc5e7..c1cba9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ dependencies = [ "jinja2>=3.1", "weasyprint>=60.0", "pydantic>=2.0", + "plotly-resampler>=0.11.0", + "pytz>=2026.1.post1", ] [project.optional-dependencies] diff --git a/src/impakt/plot/__pycache__/engine.cpython-312.pyc b/src/impakt/plot/__pycache__/engine.cpython-312.pyc index 4f49e825d721f7b16e1d2f7943ba29f457c4b89f..12a5f3a8dd4327e2a5510614c743e6fee0521bdc 100644 GIT binary patch delta 5058 zcma)AYj6|S72cJ0wbJTg$&w|%Fl!#h!VgFw2@sy)O$=#)rj446y!KtoGAqfsS_7tX z4O5yxhBk3;IwlDzAP;{na)zZ9FB#vmq^$CO`U5E~K4lr)kf*vMl4k zwCmyA*SX(4_ndRjIpXb|zdI0k-|tri_&!=~r&qO(1!_ls38Qg=2nm6R#9@jVN5Uau z-)TA%PG}`l%E$?MX3mvS5^mm?O;1KmsJ!noy%}G^$NP%u&jb>IOjV*P6HEj}!66vV zfkk-g?VnM7Nffs5xkNZ65cjKMBH|Es3dD0#AnGMYi9J!{5pMq@KH@zoC8ER5xPR#R zx8AH3N7b+Q8(FQpn;LmNGiVwc4$>i`ySrUGLiNGCX7w9dUph5J4K1D3bZyYgS>|v@ znVYKJ-8=a5{+a!>yIbqaQH=~|^-Q`~>pFPwA&umYWI22~o6>Sw?N~0CSuv<*JJl6` zMOovb?bWl{oTXdoTsFTC#?CF-VQ8x6p|elK3!~qFE#rbABt+1@gNUGQ2XUSgE0-l6 zBAs+4r1|s6K~y5cnoPW)4%bOJ;Uaz#APV%9VRyXBR=)z5+@4LPvxfeOCqBIXXab8hm$C-dwfixhe+@!ZpaZ-AQRyziySarkmz)MQH|@f$F``98T+8 zp#YNBG(DA~MvAL!u2MDQu#vU$o&2O77e(7$wpQO7KJmll3-QMNCJCM5Y{I9{f`U6M#3(9?~p8ZR%+wB3a zrt(~+Ju2qmsTW*#S{qSGxS(3iW$prEEJ86By9bt(XDjVQqx98CYJPtF*8Zds!-YN$q-=1XtJqR_kC}ykT#}5jyb<|JJln@V+K~ zUj(F;D4nRV6G|j9QCAj66rK~$G?bS;UQ*9NIhzm8XyC&y@|P>N#9e64-fn z)Im>T5mhxuleCi+)t% z-R=k8rNG-#@Xz60HkUWs2{^5N4&aFQIr<#igeTx7@Y4e$Rb`}(03a6qxe1&jpw`vFUc%}i9A z+A9u1R*}^cv%!AizVf@QaIkkg(e;(#-8K*>yho)FoVdRtZT3}^kd1iilm|!v`kPVi zDhKgrMZkHWBpcJ!sN;kW8x`i)jD)zfU!4=Q! z3LEt{bAD9c0rm^;vZ{wFZUu-3@lx_qz7u%-2g-)vYr3}~I+q^;P<)&I7K8*g?Oi>p z@&JojU<*y<*UgS_CJ$+1opunCCniijO!uXGd8{r4X5_-J9Gll^yZbcEy`{{Hxnf@G zo10eQu+h@~GM$@ZJNe1IkVOotWrp&W)~{!YY49{}>chDqYnyKA-QBmzj7YerNAEoZ z87BuxC!I0k>Sm&$dPZUI$6D>3@?A=8R$P#)YV1GXN4 zQsg#~zEqaP=O{0I9dZMd2(cO5WHy=ahe9J+DrE9(sqeRos1HnP%O>RV{6!~?@X?Vp zvHER^==uJ*#G?Mk>f;Vu>hDV)<#d?V0voME(#D?j?-Xm<+x{yxeD;2veE`WCBx~7; zz&ckAb{p7x0c`2DDCS7^QB_`yut$Q;L6YvZO44y* zSFkg|rl{utc%Nm^lxa&=I%DAVEuCEqYO9wargAiC>OF>OE5oG@-9LMt7w?dqITYUr zUC#a;+*)02OL@cW0}Ad={V2uW(87lZa-gPZ3W)9F#Y^(g5gnch#o$jj;Jkb^d001X zr`cmt6oKC@UkyEt{WR3wflAKrlmy|pFs%u`$o1;x*A~4VzYza%+q>4)>aNpUKdp|8 z$H(HV?bKGbxca#J^3e}`O&|I;v!7Hy<*3ux`f#f|MBN=8EzS!b_>DUqDu%x z#{Fad>yhYq*I3tNWaTUCflSpkoR7T_n`w%Q5Ni6nAcd-nK4Iyy*LPmn`JC_UfyrR= z)RLCDqhJ`Z&h?xhcwu0&Y0Vq!f&4MJw)j<1sK2-Ps3`b?&u)BX<5W%E_?36UU5)^j z6j&!gW)*Mtu)1iYU%IW1dU9a|N|xp9cuZ#BiH6ymQN@qS2=KHkK><@E0*ys} z5Q_#zr@I7);i2r>rBo9CK@}LxY0`jng;`cf?(DUS2!Q z&<-S>Nbo%%znMD%{I+7{vc^$s_+Z?9*FCn>mo`n?gS0ni!ec`ah!}n|J(@{dNh_B; zoHmZ^i~A|}*#sVxLy`ev%UrgS4E;D;dG`3QAgZ7&wu<=4(gBBHKCaMjA@>N9ZzDmC z^Al_@hfG>@6zX}~%K>17#k~SdGr^pG7dIq<9ILJT7U@jtgupqJrb&)|46FGfkmEuz zD1_?917m?x@~6?-@qw{{sX+AE%;^j^+0)snQ0-KtaeVLC-sd*G()vND?RuzY^z8O) zjqQ_-?U%P-TfJ>^^|oK%_xnx1*>pAZ&8b-9Ex!^|PkCsNg(vuhvvTIkd4 z-ukl#pMuNVZm?lGqo<6xT+(_0W+_ARJQ6fQ9^)fdZAq~|gyFI99L1I~93pFKXcM1i zdm3Ks;_%>R$$l$i#-%yvH*eI-8GYvo6QR>|AEV(J;Qt?q)s9!kk zT`VuV;cqR<&_eJIHxw1q$94q1?L})c|(+|B{dqk(a=5y64Z!C%7z4FY0HymP$ zt-+P_V*6a zJnSq@*Se~lEoylB-b40m=A3siGBS*}AVuO{qBIy0yrvN~rj(22E-NnPwei=w04 z>+q&rQ?@5%SX}^XPMXZEg&uKQrPzvw@RqB{{rs4^S}TA delta 3605 zcmbVPdu&tJ89(Pf{Jht3VqeF3JA{NdkO1M8wLr&smgg$aTHLbCM^Oq1(K1(AJqEhqGZvGQf5}hLboMl+#=Z+ z+6qx*GW- zAw}6D8Nb-%2{lQUTn;BC=OPL*^|A@D<(6uQpj*=oLS6!&ib_$nR8id+UR-opt_gZB zVyT&6c|&F74agTscZh~ijnJCB&9FtyfcD3qqT6z3BI3L5y4q-2W+`Svd2U9cmC=d3zogV`C0v&3TwC|vqsi5(>x7zhYuD4tOW13YAnK9U~SCO7OZVuxOSuma25?pu%c~H z8kC@NAdZeLdNEK&>ec~iF^gIO2t~W4vkphI!j{Z*PP@u_&$T-qc8O1Xwc@@v6g^1` zc=F?{i!Ei#uwIK*0O`h<>6LWROdoQVKIE`&)-&TN7wpXP@~o0#QSFcPRbKzw253eq zQ?06*6%{K-u8OWc z*|yMt(wujKs%NqAjZZ;(pPG5PT+mMPccG;FKP5D?`w3X>4i;u)Us(!H?uMonf~MeB zX4X$rF{!u8Qf3#XiS83W(U*jPTUuUfJn@aER*S*FYOyZ(^Ch;Lg;rVkj?A%X*y@Qi zGly-3B@4rG7suYfq16!4R((+7OT;&;x6`orsQS9$h*qF{B@#R(e3f{+<|!ApS6(YV zu8B3pU}}4Z^T$&>muD&4R~XKp$fkx;BaF9+w$P4U2yVMiCPxN_^W4!F_t~nMHnS9grqj+~LjXkiJot))}HL;^o>-kKtbyIJ0}MUVN{% zqjk}8*NytsS1cgE)7H#AL+X3ywp`yEeL?C5RRk{+&_?fh;QF|L z4G6i9J#ZaSaXS|Fne$f`7<;$AOM%rstXP%8OcSjQb$V5Kt0lwz0M^ik`+u?QF!g}VNz~ONuc#-feqS#bF{;=F16WhvQI-9kXp8 zH86w-2apUSISj;B9nB>T47rt4f&~c2E`K7$Z68ARJD720F*kALaU>^@AbLRof}b$_ zOUODXa?MTS@1fwwKqkoTVDv&@DsV=Nc6|``#O3)8C%QW77oqR`b4Xr9@)D9mKx|hM z&L8ypq!{dIHSoCE9%zQ6sebSdakyJt?1&y%i&N~D4vH=F)F3~Fua}WH?NvIy(w2_* z7SMDj^$u}-vh{&cUdVIov!kg3e;1Z~M6P?5)X?n{Fpqoq+c2DpVkY-P`;J+$Y`S2+ zxOo`<14(5Mm3(;()|8BJW*B^qA8PSx{)R|)8e+V&cN~wo9Zu(tCXbjs1Eu{vc(-La z7Ns4W7aavvJ$O}cC*O#}oiO#|%t->8`B-*r&w9Wb&89Z;YaqpIyMQ5`lPRT;CAw63 wP(x_j9TK@iTJMm~TV%rxvf*zoe~mN|_ go.Figure: - """Render a PlotSpec into an interactive Plotly figure.""" - fig = go.Figure() + def render( + self, spec: PlotSpec, *, resample: bool = False + ) -> go.Figure | FigureResampler: + """Render a PlotSpec into an interactive Plotly figure. + + Args: + spec: The plot specification. + resample: If True, wrap in FigureResampler for dynamic + downsampling. The caller must handle ``relayoutData`` + callbacks to trigger resampling on zoom/pan. + + Returns: + A ``go.Figure`` (default) or ``FigureResampler`` (when + ``resample=True``). + """ + fig: go.Figure | FigureResampler + if resample: + fig = FigureResampler(go.Figure(), default_n_shown_samples=1500) + else: + fig = go.Figure() if not spec.channels: fig.update_layout( @@ -67,7 +95,7 @@ class PlotEngine: # Add corridor fills first (behind data traces) for corridor in spec.corridors: - self._add_corridor(fig, corridor) + self._add_corridor(fig, corridor, resample=resample) # Add channel traces for i, ch_ref in enumerate(spec.channels): @@ -84,8 +112,6 @@ class PlotEngine: label = style.label or ch_ref.label trace_kwargs: dict[str, Any] = { - "x": ch.time, - "y": ch.data, "mode": "lines", "name": label, "line": dict( @@ -101,7 +127,18 @@ class PlotEngine: f"{label}
t=%{{x:.6f}}s
%{{y:.4f}} {ch.unit}" ) - fig.add_trace(go.Scatter(**trace_kwargs)) + if resample: + # FigureResampler: pass full-resolution data via hf_x/hf_y + # and use Scattergl for WebGL-accelerated rendering + fig.add_trace( + go.Scattergl(**trace_kwargs), + hf_x=ch.time, + hf_y=ch.data, + ) + else: + trace_kwargs["x"] = ch.time + trace_kwargs["y"] = ch.data + fig.add_trace(go.Scatter(**trace_kwargs)) # Add cursor lines if spec.x_cursors: @@ -181,33 +218,43 @@ class PlotEngine: return fig - def _add_corridor(self, fig: go.Figure, corridor: Corridor) -> None: + def _add_corridor( + self, + fig: go.Figure | FigureResampler, + corridor: Corridor, + *, + resample: bool = False, + ) -> None: """Add a corridor (tolerance band) to the figure.""" style = corridor.style - fig.add_trace( - go.Scatter( - x=corridor.time, - y=corridor.upper, - mode="lines", - name=f"{corridor.name} (upper)", - line=dict(color=style.line_color, width=style.line_width, dash=style.line_dash), - showlegend=False, - ) + upper_trace = go.Scatter( + x=corridor.time, + y=corridor.upper, + mode="lines", + name=f"{corridor.name} (upper)", + line=dict(color=style.line_color, width=style.line_width, dash=style.line_dash), + showlegend=False, ) - fig.add_trace( - go.Scatter( - x=corridor.time, - y=corridor.lower, - mode="lines", - name=f"{corridor.name} (lower)", - line=dict(color=style.line_color, width=style.line_width, dash=style.line_dash), - fill="tonexty", - fillcolor=style.fill_color, - showlegend=True, - ) + lower_trace = go.Scatter( + x=corridor.time, + y=corridor.lower, + mode="lines", + name=f"{corridor.name} (lower)", + line=dict(color=style.line_color, width=style.line_width, dash=style.line_dash), + fill="tonexty", + fillcolor=style.fill_color, + showlegend=True, ) + if resample: + # Corridors are small — pass through without downsampling + fig.add_trace(upper_trace, limit_to_view=False) + fig.add_trace(lower_trace, limit_to_view=False) + else: + fig.add_trace(upper_trace) + fig.add_trace(lower_trace) + def to_image(self, spec: PlotSpec, format: str = "png", scale: float = 2.0) -> bytes: """Render to a static image.""" fig = self.render(spec) diff --git a/src/impakt/web/callbacks/plot_callbacks.py b/src/impakt/web/callbacks/plot_callbacks.py index b0ab962..2db10ad 100644 --- a/src/impakt/web/callbacks/plot_callbacks.py +++ b/src/impakt/web/callbacks/plot_callbacks.py @@ -6,17 +6,23 @@ scripting API renders the web UI plots. Transform application uses TransformChain, making the pipeline serializable and reproducible. + +The plot figure is wrapped in a ``FigureResampler`` for dynamic LTTB +downsampling on zoom/pan events. A separate ``relayoutData`` callback +triggers resampling when the user interacts with the axes. """ from __future__ import annotations +import logging from collections import defaultdict from typing import Any import dash import numpy as np import plotly.graph_objects as go -from dash import Input, Output, State +from dash import Input, Output, State, no_update +from plotly_resampler import FigureResampler from impakt.channel.model import Channel from impakt.plot.engine import DEFAULT_COLORS, PlotEngine @@ -27,9 +33,15 @@ from impakt.transform.cfc import CFCFilter from impakt.transform.resultant import resultant_from_channels from impakt.web.state import AppState +logger = logging.getLogger(__name__) + # Module-level engine instance (stateless, safe to reuse) _engine = PlotEngine() +# Server-side storage for the current FigureResampler instance. +# Single-user desktop app — no multi-tenant isolation needed. +_current_resampler: FigureResampler | None = None + def _build_transform_chain( cfc_value: str, @@ -201,6 +213,7 @@ def _build_plot_spec( def register_plot_callbacks(app: dash.Dash, app_state: AppState) -> None: """Register all plot-related callbacks.""" + global _current_resampler @app.callback( Output({"type": "plot-graph", "index": 0}, "figure"), @@ -231,6 +244,8 @@ def register_plot_callbacks(app: dash.Dash, app_state: AppState) -> None: corridors_data: list[dict[str, Any]] | None, x_align_value: float | None, ) -> go.Figure: + global _current_resampler + if not selected_keys: selected_keys = [] @@ -245,4 +260,25 @@ def register_plot_callbacks(app: dash.Dash, app_state: AppState) -> None: ) spec = _build_plot_spec(channels, cursor_x1, cursor_x2, corridors_data) - return _engine.render(spec) + fig = _engine.render(spec, resample=True) + + # Store the resampler for relayoutData callbacks + if isinstance(fig, FigureResampler): + _current_resampler = fig + + return fig + + @app.callback( + Output({"type": "plot-graph", "index": 0}, "figure", allow_duplicate=True), + Input({"type": "plot-graph", "index": 0}, "relayoutData"), + prevent_initial_call=True, + ) + def resample_on_zoom(relayout_data: dict[str, Any] | None) -> Any: + """Resample traces when the user zooms or pans. + + FigureResampler intercepts relayoutData events and returns a + Patch that updates only the visible trace data — no full re-render. + """ + if _current_resampler is None or relayout_data is None: + return no_update + return _current_resampler.construct_update_data_patch(relayout_data) diff --git a/src/impakt/web/components/channel_grid.py b/src/impakt/web/components/channel_grid.py index 801ba2d..6c07bcd 100644 --- a/src/impakt/web/components/channel_grid.py +++ b/src/impakt/web/components/channel_grid.py @@ -13,17 +13,59 @@ without reordering. from __future__ import annotations +import base64 import fnmatch from typing import Any import dash_bootstrap_components as dbc +import numpy as np from dash import dash_table, dcc, html from impakt.channel.lookup import MAIN_LOCATIONS, MEASUREMENTS +from impakt.channel.model import Channel from impakt.plot.engine import DEFAULT_COLORS from impakt.web.state import AppState +def _sparkline_svg(ch: Channel, width: int = 60, height: int = 16) -> str: + """Generate a tiny inline SVG sparkline for a channel. + + Returns a base64-encoded data URI suitable for use in a Dash + DataTable markdown cell: ``![](data:image/svg+xml;base64,...)``. + """ + data = ch.data + n = len(data) + if n < 2: + return "" + + # Downsample to ~60 points for the sparkline + if n > 60: + indices = np.linspace(0, n - 1, 60, dtype=int) + data = data[indices] + n = 60 + + # Normalize to SVG coordinate space + dmin = float(np.min(data)) + dmax = float(np.max(data)) + drange = dmax - dmin + if drange == 0: + drange = 1.0 + + xs = np.linspace(1, width - 1, n) + ys = height - 1 - ((data - dmin) / drange) * (height - 2) + + # Build SVG polyline path + points = " ".join(f"{x:.1f},{y:.1f}" for x, y in zip(xs, ys)) + svg = ( + f'' + f'' + f"" + ) + + encoded = base64.b64encode(svg.encode()).decode() + return f'' + + def _build_channel_rows(app_state: AppState) -> list[dict[str, Any]]: """Build flat row data for the channel grid. @@ -63,6 +105,7 @@ def _build_channel_rows(app_state: AppState) -> list[dict[str, Any]]: "test_id": test_id if multi_test else "", "iso_code": ch.name, "description": desc, + "spark": _sparkline_svg(ch), "unit": ch.unit, "min": f"{float(ch.data.min()):.2f}", "max": f"{float(ch.data.max()):.2f}", @@ -115,6 +158,7 @@ def build_channel_grid(app_state: AppState) -> dbc.Card: {"name": "#", "id": "ch_num", "type": "numeric"}, {"name": "ISO Code", "id": "iso_code"}, {"name": "Description", "id": "description"}, + {"name": "", "id": "spark", "presentation": "markdown"}, {"name": "Unit", "id": "unit"}, {"name": "Min", "id": "min", "type": "numeric"}, {"name": "Max", "id": "max", "type": "numeric"}, @@ -207,6 +251,7 @@ def build_channel_grid(app_state: AppState) -> dbc.Card: id="channel-grid", columns=columns, data=rows, + markdown_options={"html": True}, row_selectable="multi", selected_rows=[], sort_action="native", @@ -255,6 +300,12 @@ def build_channel_grid(app_state: AppState) -> dbc.Card: "width": "50px", "textAlign": "center", }, + { + "if": {"column_id": "spark"}, + "width": "68px", + "textAlign": "center", + "padding": "2px 4px", + }, {"if": {"column_id": "min"}, "width": "65px", "textAlign": "right"}, {"if": {"column_id": "max"}, "width": "65px", "textAlign": "right"}, {"if": {"column_id": "test_id"}, "width": "60px"}, diff --git a/uv.lock b/uv.lock index 9730b51..f20c5ef 100644 --- a/uv.lock +++ b/uv.lock @@ -535,7 +535,9 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, { name = "plotly" }, + { name = "plotly-resampler" }, { name = "pydantic" }, + { name = "pytz" }, { name = "pyyaml" }, { name = "scipy" }, { name = "weasyprint" }, @@ -563,7 +565,9 @@ requires-dist = [ { name = "numpy", specifier = ">=1.24" }, { name = "pandas", specifier = ">=2.0" }, { name = "plotly", specifier = ">=5.18" }, + { name = "plotly-resampler", specifier = ">=0.11.0" }, { name = "pydantic", specifier = ">=2.0" }, + { name = "pytz", specifier = ">=2026.1.post1" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "scipy", specifier = ">=1.10" }, { name = "weasyprint", specifier = ">=60.0" }, @@ -932,6 +936,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] +[[package]] +name = "orjson" +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -1110,6 +1182,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0", size = 9898444, upload-time = "2026-04-09T20:36:39.812Z" }, ] +[[package]] +name = "plotly-resampler" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dash" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "pandas" }, + { name = "plotly" }, + { name = "tsdownsample" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/5c/e3990691542d789b8c07a5c021ace489b5b046d7ff66357c67536c535fe4/plotly_resampler-0.11.0.tar.gz", hash = "sha256:e3f9ceeb5749de9a85d39755f50711a7d6a28eb1b284da250251c8896ccc1cfd", size = 57400, upload-time = "2025-08-29T12:15:49.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/df/921eff232a2a8da7bdf84688b8cacfd9cef1bdc8fe695c547f7994804bee/plotly_resampler-0.11.0-py3-none-any.whl", hash = "sha256:5ce05a2b7eac5ec00379b219249ea583715077db9268fd4f4423715ce90cf653", size = 82684, upload-time = "2025-08-29T12:15:48.027Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1309,6 +1398,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1580,6 +1678,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tsdownsample" +version = "0.1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/bf/7590ae93ee6970876a79ab08602e4098a0c00db4b19f813d87935bb16607/tsdownsample-0.1.4.1.tar.gz", hash = "sha256:cc2e2fc0031a5fb75c0f5204b498e86eae0333bcc19da481c0859f1943690f3b", size = 555937, upload-time = "2025-01-29T13:13:25.185Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/57/1b0fb3bfd1ddd4bb97490e8fe5494b592028ec9fed98079d872ecb135226/tsdownsample-0.1.4.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d93a7de6e684e2f314444f75d2100e43dc76deed5e6cfd94bf6ecff0a2270383", size = 1266928, upload-time = "2025-01-29T13:10:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9a/1b1327238914ffb5e8ce4001b744df0e36a903fdfe0796e0309e86cfe1cd/tsdownsample-0.1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2569bffb94a24c781623ded653a1e50ef9f2ab102cc0be091add3dfd534338e", size = 1166982, upload-time = "2025-01-29T13:10:59.257Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9e/ebdcc469bbe9e7d2decbc23cb835043c927acd7b8e8103cafced54e722cb/tsdownsample-0.1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab0d54355b292be0426ec97a36cf8ebdd22d69b0f9e08154d7a81eea30074d52", size = 1237143, upload-time = "2025-01-29T13:11:01.218Z" }, + { url = "https://files.pythonhosted.org/packages/31/a5/497f68a50e8e1fd7828af74e9534c591a36e8ab24a20814f47a1708115b2/tsdownsample-0.1.4.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:814eca271f2a8178ad5f614c13b6093e477fa905c61f00f292bcb77e152f2139", size = 1332403, upload-time = "2025-01-29T13:11:03.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6e/b0239c632d110f0fde3289b1730543286397fb18c2a04572e74aef7fa51a/tsdownsample-0.1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97538b126a733ca88b00466580ac6f123e9a639c297058628791e05d9512369d", size = 1322541, upload-time = "2025-01-29T13:11:05.21Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b7/83ec7fac8396fad3918bb71baf9286107b4d6335e06417fae244b1250a8a/tsdownsample-0.1.4.1-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:1c6e1c2422b93ec2c3962e04fb10595709b894483918ef7977d2f84f37d6acf7", size = 1258659, upload-time = "2025-01-29T13:11:08.778Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a3/8e9ff9f0191aece44581dc0b94a03ee3fe48cfb8650a32a1cec4948043e2/tsdownsample-0.1.4.1-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:8af543398045920bc3e7078eab627934069ba74d99a49d3dd62dda5c5c5c8499", size = 1269297, upload-time = "2025-01-29T13:11:11.044Z" }, + { url = "https://files.pythonhosted.org/packages/12/1b/e077b7c4b4813ac40c9b9e429138afcaf35037303207c3cf065746630d24/tsdownsample-0.1.4.1-cp311-cp311-manylinux_2_24_s390x.whl", hash = "sha256:036e783d81e773ddba377a45ac19489b365b6aac16e915ed31fe6f5260b588c8", size = 1448877, upload-time = "2025-01-29T13:11:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/07/df/fef81e7b9f86d68f704824527bbc5d064059a94b03d46635cdfc8d42e534/tsdownsample-0.1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0afc76e88ad51a0887323823dd96cbe1a49300c543c9baa7cb93456c4bf69e11", size = 1398147, upload-time = "2025-01-29T13:11:17.866Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/779747b031404739977f5996260e7386298c983c3820c5f79fd4b28257b6/tsdownsample-0.1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46a109da21a3be8ef81cc3b988824309bbf1ea89e6385468911f46ee6f308d85", size = 1523919, upload-time = "2025-01-29T13:11:21.484Z" }, + { url = "https://files.pythonhosted.org/packages/cb/af/e847569c61bc58bd9ba41f24493d5159d8d42fdbd36a2e0039255196107a/tsdownsample-0.1.4.1-cp311-cp311-win32.whl", hash = "sha256:d6232c1c0e0e5cf49e57de2a860bb0c6e1279da4e1d360ab2bd456b25fef6a7d", size = 773506, upload-time = "2025-01-29T13:11:23.735Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/a3d611c83b2c647451ffbbbce3d589c377f9de990a616eda17c459a74d83/tsdownsample-0.1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:3378ef3ac19557191400697c8fb2586917fd42cc73099eab32a8a7874a48c417", size = 1034102, upload-time = "2025-01-29T13:11:27.735Z" }, + { url = "https://files.pythonhosted.org/packages/01/5c/1bc005378e4a7f0738c5ca093f7b2893eac932d44a59be6331b373119c88/tsdownsample-0.1.4.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:13aa99d40f0e25e49a28fd540fe13759cc19ade788013d46922a1cb60f475ff1", size = 1252185, upload-time = "2025-01-29T13:11:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/4b12eeb6119389c83a7618e7bbfb9b3f6fc2ae6fa271d978982ebabede7f/tsdownsample-0.1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d177219c46d9499e28a9b637a6f2d87ccbeb82e1cf57d9a5e68c5fe85df33fe", size = 1166653, upload-time = "2025-01-29T13:11:32.104Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3d/52e84bc71b1f7f497953563055c82f7a8bf975291f13f248f219f94cedc9/tsdownsample-0.1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbed20c6dbbe029ae380cc01cc5ddc0db74aa65d57b26302ec4c3f92744efdd8", size = 1231341, upload-time = "2025-01-29T13:11:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/150984d8abef039ca48562cc8ef8dbf6a955aeba88a48dc9bb4f5e15f86a/tsdownsample-0.1.4.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7b780d155e2ef47b36db09b9c166512c153c533fe22ff24d5b351cdab9a76b1", size = 1312409, upload-time = "2025-01-29T13:11:36.904Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/52552bfe812c872660875d82e7a142558d917e0c25196c8c7a6d54d6eb67/tsdownsample-0.1.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f941f9da83d436516546251ca4145e5748b7fa20f1f5f76d49bb924f2af87b35", size = 1325040, upload-time = "2025-01-29T13:11:41.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/39181f51b37c864decbfb8f778b7ebf78f54048fb701fabb800fd62b0930/tsdownsample-0.1.4.1-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:49ce60fa007e25b9a4889110a24a10ea96a0f568647d74e263cf7dd325c746d1", size = 1260784, upload-time = "2025-01-29T13:11:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/fe/dc9efb88c8913149439b7ede45b6e4ca5cc3d585edded50c850f9a8a7abf/tsdownsample-0.1.4.1-cp312-cp312-manylinux_2_24_ppc64le.whl", hash = "sha256:656f7b1c27ba457fb2af2b87451925887bce4c7990a1cc43e8b58bcbe56ab729", size = 1286794, upload-time = "2025-01-29T13:11:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/3546fbf09a6dddbf6362b252ee01cd9ee2a2695828991dae0611f51f19d0/tsdownsample-0.1.4.1-cp312-cp312-manylinux_2_24_s390x.whl", hash = "sha256:498c99c512bccc8cb77c823340b73ba2f0b07c392f0091f17ce172a426eeb007", size = 1444825, upload-time = "2025-01-29T13:11:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/13/fb/39cd6067bb833a1d478560070002fa2737785a9dd8fc3fe231cafae5b0b7/tsdownsample-0.1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:24d6e29cc89fa1ab836df9c9590fd4f26088c661e3ed9f5add966c5b076df6fd", size = 1392808, upload-time = "2025-01-29T13:11:51.152Z" }, + { url = "https://files.pythonhosted.org/packages/4a/31/0db2e5a3ff868f6b09a1ba4e87d565c4552596e6f2c16e15c3fe29f6000b/tsdownsample-0.1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9afa84ec630081ce87060d2f0c294df084da2627ac2b623d36dfcfaef02005a", size = 1527173, upload-time = "2025-01-29T13:11:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/70/cf/3a407a40ac6b9e08f6e59846b5a4b5a68797f0c604219deace02a5c9fe16/tsdownsample-0.1.4.1-cp312-cp312-win32.whl", hash = "sha256:128abd5c326f7a41436ec3f450c9ab128558498bf0abc67a760a94d2ac3a68b4", size = 767553, upload-time = "2025-01-29T13:11:55.966Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6b/d80ef005f1ccbb5a325d4daf9e48d09f39ffcb20c65108f4eafb89b88510/tsdownsample-0.1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:9138b330c0172211bdd34fca24947fb35648784046f4d08e32f7fb97d775ad96", size = 1029860, upload-time = "2025-01-29T13:11:58.474Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d8/b59fde996a4371e719a2cdb8bc16990beaed737b97de71e716a186c8546a/tsdownsample-0.1.4.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0a3dcac6f49f9543c5e12caab5b3c5ed78dafd76afeb70ea71453b5821f0fede", size = 1252057, upload-time = "2025-01-29T13:12:01.235Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/3f8f18fa5f4b1b899c4a81b0eadb4ba22014adc70d22f2c9eff1ba3e0c80/tsdownsample-0.1.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3b690075a240478de70d512b50db165e7ef98b9e6f7d55e101146dbf3a09b1", size = 1166465, upload-time = "2025-01-29T13:12:02.866Z" }, + { url = "https://files.pythonhosted.org/packages/53/2c/253e00fa376ca3cc8b6eb42fee144be70824e80150742d9fae5323533421/tsdownsample-0.1.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263fd9259e8a6541d5c1e2744e90b38c8d6f98f343b03a19951fbf94fb10cb43", size = 1231028, upload-time = "2025-01-29T13:12:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/0ff315a337d176737f6ffc05af4190f8cf9d344d71b64e2eb81bb2ca7347/tsdownsample-0.1.4.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fec09afa83d162a560fb902a60ec4f5b4cbcbbe3eb595c5c75bc28571dcf03c", size = 1311979, upload-time = "2025-01-29T13:12:06.664Z" }, + { url = "https://files.pythonhosted.org/packages/a4/31/7a572efe29e678c2bb172bf93f14a7fa14f4ecc421e36700a48447f99b96/tsdownsample-0.1.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92629086cf799f0364e8e70996de9e3724f738ab2ab323d9228fc1275c8c0adb", size = 1325055, upload-time = "2025-01-29T13:12:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/c7/05/7c3e32662f6059b3c739c6528d3c1eece57ff3955168dc483a37a6a1b576/tsdownsample-0.1.4.1-cp313-cp313-manylinux_2_24_armv7l.whl", hash = "sha256:a917c885998c851eb1e6ca58e8f19a2f2f4e09dc6b6719bd1755bcc6247e98eb", size = 1259867, upload-time = "2025-01-29T13:12:11.362Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/1e97e37cc4cec3ee1e5ad1934ea75d83e41a8258686adc7b2309bf32ca84/tsdownsample-0.1.4.1-cp313-cp313-manylinux_2_24_ppc64le.whl", hash = "sha256:e93179108e9721d637151f0645e3b4f50819446e4458156103472b35ffaa026d", size = 1286896, upload-time = "2025-01-29T13:12:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bd/f93db320eb21660a443e849174423aa9324dc5cba016c096255bdc3c17a7/tsdownsample-0.1.4.1-cp313-cp313-manylinux_2_24_s390x.whl", hash = "sha256:26e74a6a9591bc52310ce16c316efe59a2443f7718188d459c5a2e5fb58883a9", size = 1445464, upload-time = "2025-01-29T13:12:15.415Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0f/2711e5e24071b55fa653b13bbbbafad08f96832d92094c3c6f3368d986a3/tsdownsample-0.1.4.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c3953f484205b70e7a4e8473e2cc96a3ef6a24eb6600b89980f47447cbff4492", size = 1392698, upload-time = "2025-01-29T13:12:17.836Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/a7cce9d82017b5bbf3629acab08e58dafedd24f602b603c3c7f528bd8dd6/tsdownsample-0.1.4.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fd5b786931d6500938942398e0aadb44046366ac28460ea008181ea75fb6dd52", size = 1527159, upload-time = "2025-01-29T13:12:19.687Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/49aac73ea3702bab30c6c4e29344ccfe3fcfbc20df739e3bbda4e571c258/tsdownsample-0.1.4.1-cp313-cp313-win32.whl", hash = "sha256:e7720aad79b6b76a1cdd20c2931e8e60e0465b437dc07c567d090493f39c36a5", size = 767855, upload-time = "2025-01-29T13:12:21.465Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a3/8e4b211b81559fa6ae3bb98dc08bfa68e7b5e60e5ad394da12064951e8d0/tsdownsample-0.1.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:7985be2da94507bc507982c3f868b34b8bb70c84ab80ca53a9e8756ca91758c8", size = 1029209, upload-time = "2025-01-29T13:12:24.842Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"