Feature Comparison & Design-Phase Scoping
Kickoff issue for lab-next — the next-generation graphtool that could supersede both lab (the legacy vanilla-JS CrinGraph fork still powering most squig.link sites) and modernGraphTool (the Svelte 5 rewrite).
This document is a structured feature-by-feature comparison with an opinionated recommendation column. Its job is to give the design phase a concrete starting surface — every row is a decision we can ratify, amend, or defer.
Why lab-next (should) start from modernGraphTool
Two starting points were on the table: lab — the long-running CrinGraph fork that still powers most squig.link sites and is familiar to every returning visitor — or modernGraphTool, the ground-up rewrite I've invested in over the past year. I think we should build lab-next on top of modernGraphTool for two reasons:
- The hard parts of a graph tool are already done. lab is a single 377 KB file with graph rendering, measurement parsing, curve math, smoothing, normalization, and state management all tangled together. Pulling those pieces apart into something we can evolve safely — without every small change risking a regression somewhere unrelated — was a year of careful work, and modernGraphTool is the result. Starting lab-next from lab would mean doing that same extraction a second time before we could even begin on anything new.
- What's left is a design problem, not an engineering one. Compared to lab's simple, one-screen directness, modernGraphTool's interface is busier than a first-time visitor wants — more panels, more options, more to learn. That's a UI we can trim and rearrange incrementally; it's a smaller task than replacing the whole codebase.
§1 Scope of comparison
| Dimension |
lab |
mGT |
| Branch / state |
pink-eq branch — latest, introduces graphic-EQ presets, revised auto-EQ constraints, live music-spectrum overlay |
main |
| Tech |
Vanilla JS + D3, no build step, single ~377 KB graphtool.js |
SvelteKit 2 + Svelte 5 runes, TypeScript strict, Tailwind v4 |
| Deployment |
FTP-drop HTML, per-operator hosting |
npm run build → static dist/ or CDN dist-cdn/ |
| squig.link services |
External scripts hosted centrally at squig.link (squigsites.js, shoplinks.js, squiglink-intro.js) — each operator's lab page loads them via <script src="https://squig.link/..."> tags and the scripts attach themselves to the DOM at runtime |
Bundled in-app, domain-gated via squiglink-store |
§2 Feature comparison tables
2.1 Graph display & interaction
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Log frequency X-axis (20 Hz – 20 kHz) |
✅ |
✅ |
✅ Keep |
Non-negotiable. Industry standard. |
| Linear dB Y-axis with configurable scale |
✅ preset buttons |
✅ 30/40/50/60/80 dB presets |
✅ Keep |
Both use preset buttons; mGT's list is fine. |
| Zoom-to-band presets (bass / mid / treble) |
✅ |
❌ |
❓ Open |
Not sure how many users are acually using this feature. |
| Curve hover highlight |
✅ |
✅ |
✅ Keep |
Essential. |
| Inspector / numeric readout on hover |
✅ toggle mode |
✅ GraphInspection.ts overlay |
✅ Keep |
Both already have it. |
| Curve-drag interaction (adjust offset by dragging the curve) |
✅ drag-slider on the graph |
✅ GraphHandle.ts drag handles |
✅ Keep |
Both have it. |
| In-graph labels |
✅ labelsPosition, stickyLabels |
✅ configurable corner + offsets |
✅ Keep |
Both ship; mGT config is more granular. |
| Watermark (text + image) |
✅ watermark(svg) function |
✅ WATERMARK config array |
✅ Keep |
Operator-customizable. Carry forward mGT's data-driven config rather than lab's function-body approach. |
| Rig description label |
✅ rig_description |
✅ RIG_DESCRIPTION |
✅ Keep |
Essential for operator trust — shows what rig measurements are on. |
| Screenshot / PNG export |
✅ saveSvgAsPng.js |
✅ ScreenshotButton |
✅ Keep |
Essential sharing primitive. |
| Animated curve entry transition |
✅ alt_animated (toggleable) |
✅ |
✅ Keep |
Default-off per lab precedent; respect prefers-reduced-motion. |
| Sticky graph when scrolling |
✅ alt_sticky_graph |
✅ AppShell-level sticky |
✅ Keep |
Desktop ergonomics; mobile is always stacked anyway. |
| Aspect ratio (16:9 vs. CrinGraph 800×346) |
✅ CrinGraph ratio only |
✅ ASPECT_RATIO config with both |
✅ Keep mGT |
Operator choice; mGT already parameterized. |
| Live music spectrum overlay |
✅ on pink-eq (graphtool.js:1148 — music-spectrum-viz, analyser) |
✅ GraphSpectrumOverlay |
✅ Keep |
Both ship it. |
| HpTF deviation visualization (fill / curves / both) |
❌ |
✅ configurable |
❓ Open |
Advanced. Not sure how many operators are interested in it. Requires some adjustment to measurements API. |
| PreferenceBound upper/lower overlay |
❌ |
✅ PreferenceBound feature |
❓ Open |
Research-rooted but visually busy. Decide: default-on, default-off, or drop entirely. |
| FrequencyTutorial band overlay |
✅ alt_tutorial with labeled regions |
✅ FrequencyTutorial feature |
✅ Keep |
Great onboarding for new users; both tools have it. |
| DragDivider resizable graph / panel |
❌ |
✅ desktop only |
❓ Open |
Useful for manipulating busy UI, especially EQ panel. |
2.2 Selection (phones, brands, targets)
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Two-tab brand / model selector |
✅ |
✅ PhoneSelector + brand panels |
✅ Keep |
Core CrinGraph UX; carries familiarity. |
| Fuzzy search (Fuse.js) |
✅ |
🔽 local filter |
✅ Keep + upgrade |
lab's fuzzy matching is materially better. Might able to port Fuse.js (or equivalent) into lab-next. |
| Exclusive vs. additive (ctrl/middle-click) select |
✅ |
❌ |
❓ Open |
mGT always add devices by default. |
| Phone-list augmentation (review score, price, shop link) |
✅ listAugment.js |
✅ |
❓ Open |
|
| Variants (multiple measurement files per model) |
✅ file:["A","B"] + suffix/prefix |
✅ Identical |
✅ Keep |
Essential for models with multiple samples or seal conditions. |
| Pin / lock selection |
✅ PIN button |
❌ |
❓ Open |
Pin / lock selection isn't that meaningful with additive selection |
| Target selector grouped by type |
✅ targets with type buckets |
✅ TARGET_MANIFEST with i18n |
❓ Open |
mGT's manifest format is cleaner, but not many users are interested in i18n. |
| TargetCustomizer (Tilt / Bass / Treble / Ear) |
❌ |
✅ TargetCustomizer |
❓ Open |
Powerful but dense. Many users seem to want this feature, but it's still absent in lab. |
| Target-last-used restore |
✅ targetRestoreLastUsed |
✅ settings persistence |
✅ Keep |
|
| Cross-site search |
✅ via squigsites.js (registry select in header) |
✅ CrossSiteSearchResults (live JSON fetch) |
❓ Redesign |
See §2.11 — the data-source model is itself a design call. |
2.3 Curve manipulation
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Hz-target normalization |
✅ default_normalization: "Hz" with default_norm_hz (default 500 Hz) |
✅ NORMALIZATION.TYPE: "Hz" |
✅ Keep |
Standard across measurement tools; both support it. |
| dB-target (loudness) normalization |
✅ default_normalization: "dB" with ISO 226 + free-field weighting |
❌ (mGT has only Hz + Avg) |
❓ Probably drop |
Raw measurement data is conventionally calibrated to a known SPL (e.g. 94 dB @ 500 Hz); users expect curves that preserve that calibration. Shifting curves to a chosen loudness target obscures the original data and can mislead. mGT already made the call to omit it. Lean toward drop, but see §2a for the full argument. |
| Midrange-average normalization (300–3000 Hz) |
❌ |
✅ NORMALIZATION.TYPE: "Avg" |
✅ Keep |
mGT-only, implemented as a alternative for dB-target normalization; a practical default for reviewers and a reasonable default for lab-next. |
| Smoothing (parameterized) |
✅ smoothing spline, scale_smoothing |
✅ 1/48, 1/24, 1/12, 1/6, 1/3 octave presets |
❓ Decide |
Different math — see §2a. |
| Baseline (flatten one curve) |
✅ BASELINE toggle |
✅ baseline UUID in store |
✅ Keep |
CrinGraph classic; keep lab's terminology. |
| L / R / Avg channel selector |
✅ "wishbone" UI |
✅ SampleChannelSelector |
❓ Open |
lab's wishbone is a iconic UI, but it can be hard to use in small mobile devices. mGT uses OS native selector UI. |
| Multi-sample display (num_samples > 2) |
✅ num_samples in config.js |
✅ samples in phone_book.json |
❓ Open |
Required for headphone measurement sets. mGT expects the num_samples equivalent data from phone_book.json. This change was made to accomodate various devices with various amount of samples. Might be able to add default num_samples in config.js for compatibility. |
| Channel imbalance (!) indicator |
✅ max_channel_imbalance (Kadane's algo) |
❌ |
❓ Open |
Kadane's algo catches sustained, cumulative imbalance. Useful, but it's a non-IEC heuristic. We can either import Kadane math and rename it into "sustained L/R divergence", or implement the IEC 60268-7 §8.8 point-wise max |L−R| in real dB. I personally think imbalance indicator isn't that useful though. |
| Per-curve yOffset (manual dB offset) |
✅ numeric input in manager |
✅ |
✅ Keep |
Essential; both have it. |
| Hide / show without removing |
✅ visibility toggle |
✅ |
✅ Keep |
Essential; both have it. |
| Algorithmic curve colors (HCL low-discrepancy) |
✅ sophisticated |
✅ |
✅ Keep |
lab's algorithm is genuinely good; either impl is fine. |
| Per-curve manual color picker |
❌ ("recolor" button only) |
✅ GraphColorPicker popover |
✅ Keep |
mGT uses OS-native color picker UI. |
2.4 Equalizer (parametric / graphic)
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Parametric EQ editor (PK / LSQ / HSQ) |
✅ 10 bands default, 20 max |
✅ EqFilterList + EqFilterCard |
✅ Keep |
Core value-prop. Use mGT's implementation — AutoEQ + Device PEQ depend on it. |
| AutoEQ (auto-fit filters to target) |
✅ |
✅ EqAutoEq + EqAutoEqSelect |
✅ Keep |
Both ship. |
| Preamp calc (prevent clipping) |
✅ |
✅ tracked in eq-store |
✅ Keep |
Required whenever EQ applies gain. |
Filter import / export (.txt biquad / Wavelet) |
✅ both formats |
✅ |
✅ Keep |
Standard community file formats. |
| Real-time audio preview (toggle EQ on playback) |
✅ |
✅ EqAudioPlayer |
✅ Keep |
Lets users hear EQ changes immediately. |
| EQ graph normalization option |
❌ |
✅ linkEqNormalization flag |
✅ Keep |
Niche, but people have different opinion about it. |
2.5 Audio playback (non-EQ)
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Pink noise through EQ |
✅ extraPinkNoiseEnabled |
✅ |
✅ Keep |
Simple, cheap, useful for EQ tuning. |
| Tone generator |
✅ extraToneGeneratorEnabled |
✅ |
✅ Keep |
|
| Local music player (file upload + loop region) |
✅ extraMusicEnabled |
✅ EqAudioPlayer |
✅ Keep |
|
| Live spectrum overlay (on graph) |
✅ on pink-eq |
✅ GraphSpectrumOverlay |
✅ Keep |
|
2.6 Sharing & persistence
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Shareable URL |
✅ ?share=Phone1,Phone2 (query string) |
✅ Base62-compressed state |
✅ Keep mGT compression |
mGT's URLs are much shorter for complex states. |
init_phones default load |
✅ |
✅ INITIAL_PHONES / INITIAL_TARGETS |
✅ Keep |
Operator-configurable startup state. |
| URL auto-update on state change |
✅ share_url |
✅ URL.AUTO_UPDATE_URL |
✅ Keep |
Enables back/forward nav. |
| Copy-URL button with feedback |
✅ |
✅ |
✅ Keep |
Trivial and essential. |
| PNG screenshot |
✅ saveSvgAsPng.js |
✅ ScreenshotButton |
✅ Keep |
Covered above. |
| Settings persistence (localStorage) |
✅ theme, EQ presets, playback segments |
✅ settings-store with local/session modes |
✅ Keep |
|
| Export CSV per phone |
✅ |
❌ |
❓ Open |
Not sure how many people want this feature, but it can be implemented as an option. |
2.7 Theming
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Dark / light mode toggle |
✅ themingEnabled (stylesheet swap: dark.css / white.css) |
✅ .dark class on <html> + OKLCH tokens |
✅ Keep mGT model |
Token-based is infinitely more maintainable than two-file swap. |
| Operator color customization |
❌ (must edit CSS file) |
✅ CSS vars in theme.css (but still self-host only) |
❓ Open |
Multi-tenant requirement. Operators pick a palette/logo/watermark from a UI, not by editing CSS. Build on mGT's OKLCH token system. |
| Target curve styling (dashed, color, custom) |
✅ targetDashed, targetColorCustom |
✅ TRACE_STYLING |
✅ Keep mGT config |
Data-driven config > hardcoded CSS. |
| Label positioning |
✅ 4 corners |
✅ 4 corners + offsets |
✅ Keep mGT |
More granular. |
| Sticky labels |
✅ stickyLabels |
✅ |
✅ Keep |
Good for scrolling graphs. |
| Font choice |
❌ system default |
✅ Pretendard via @theme |
🔽 Tenant-configurable |
Should be a per-tenant setting, not a hardcoded import. |
2.8 squig.link integration
Delivery model is itself a comparison axis. There are now three of them, not two:
- lab — external
*.js scripts hosted on squig.link, pulled by every operator's index.html and attached to the DOM at runtime. Hot-updatable across the whole network with no rebuild.
- mGT — bundled in-app code, domain-gated via
squiglink-store. Type-safe and testable; changes require a release.
- Squiglink API (WIP) — the new centralized backend at
server. Capabilities become REST resources rather than runtime-injected scripts or bundled client code. Today the API covers the data half (brands, models, measurements, evaluations) via GET /brands, GET /models, GET /legacy/data/phone_book.json, etc., but has no endpoints for the integration-service half (sponsor/shop-link, welcome content, theming, cross-database search).
Each row below calls out which of the three supports it today.
| Feature |
lab |
mGT |
Squiglink API |
Recommendation |
Rationale |
| Cross-site selector / search |
✅ squigsites.js reads squigsites.json, builds a <select> grouped into 5128 / IEMs / Headphones / Earbuds / Official, injected into ul.header-links |
✅ CrossSiteSearchResults fetches squigsites.json directly, renders native |
⚠️ partial — GET /brands, GET /models span all databases but there's no unified "devices across all operators" endpoint; cross-site metadata (site name, URL shape, dbType) isn't modeled |
❓ Open |
Both work, but differ in delivery model. |
| Shop-link sponsor |
✅ common/shoplinks.js — MutationObserver on #fr-graph, looks up active phone in shoplinks.json, fetches live per-product JSON from sponsor for current pricing, inline data:image/svg+xml sponsor logo, UTM-tagged links |
✅ ShopLink feature |
❌ missing — no sponsor/shop-link resource in schema; content lives as static JSON + eval()'d JS on squig.link today |
❓ Redesign |
Hosting this as an API resource would be a better choice. |
| Welcome / intro slideshow |
✅ common/squiglink-intro.js — 6+ slides with MP4 animations, embed-mode awareness, squig.link-domain cookie scope |
✅ SponsorBanner + TutorialModal |
❌ missing — no welcome/intro resource in schema |
✅ Keep mGT |
Native Svelte is easier to maintain than script-injected slideshow. |
2.9 i18n
| Feature |
lab |
mGT |
Recommendation |
Rationale |
| Multi-language UI strings |
❌ English only |
✅ Paraglide compile-time (en, ko) |
❓ Open |
lab-next could ship with more languages for core elements, but it's tricky to localize user configurable data. |
| i18n in operator config (TOPBAR, TARGET_MANIFEST, DESCRIPTION) |
❌ |
✅ resolveI18nValue() |
❓ Open |
Operator-controlled multilingual labels. Could be kept as optional. |
| System language auto-detection |
❌ |
✅ ENABLE_SYSTEM_LANG_DETECTION |
❓ Open |
Default-on. |
| In-app language switcher |
❌ |
✅ MiscPanel dropdown |
❓ Open |
Always provide manual override. |
2.10 Data & config
| Feature |
lab |
mGT |
Recommendation |
Rationale |
phone_book.json format |
✅ brand/phones with file/suffix/prefix |
✅ compatible |
✅ Keep format |
Breaking this breaks every existing operator's data. |
tsvParse flexibility |
✅ accepts REW / AudioTool / CSV / TSV |
✅ |
✅ Keep |
Ingestion robustness matters. |
default_channels / num_samples |
✅ |
✅ |
✅ Keep |
Required for headphone configs. |
| Target manifest with i18n |
❌ flat targets array |
✅ TARGET_MANIFEST with i18n labels |
❓ Open |
Users might not be interested in i18n. |
config.js surface |
~38 live flags |
~20 grouped sections |
❓ Open |
In a centralized lab-next, operator-tunable config should be a short UI form, not a JS file. Self-hosting path keeps the file for compatibility. |
| Live per-product price fetch (shop-link) |
✅ JSON |
✅ |
✅ Keep |
Platform-owned already. Works fine via central JSON endpoint. |
2.11 Squiglink API coverage for lab-next features
This table audits the WIP Squiglink API (server) against the feature-level decisions above. It is scoped to feature/endpoint-level asks.
| Feature area |
API status |
Gap |
Recommendation |
| Device browsing — brands + models with pagination & search |
✅ works today — GET /brands, GET /models both paginated and trigram-FTS searchable |
Page size hardcoded to 10, no limit/offset query params |
No API change needed for MVP lab-next. Might need page_size later. |
| Measurement load for a single device |
✅ works today — GET /legacy/data/phone_book.json?database_id=X emits CrinGraph-shape JSON; GET /legacy/data/{uuid} (L|R).txt returns channel CSV |
Unknown database_id silently returns [] instead of 404 |
Could be better with 404 semantics? |
| Cross-database / cross-operator device search |
⚠️ partial — /brands and /models already span all databases (brands/models are global), but there's no endpoint that returns "every measurement matching this query, with source operator + database metadata attached" |
No unified /search/devices that joins brands × models × databases × users; no way to filter by DatabaseKind across sites |
Might require a search endpoint that returns results joined with databases.user_id, users.username, databases.kind, databases.path to reconstruct today's squigsites.json-based UX. |
| Targets (HRTF reference curves) |
❌ missing — no targets entity in the schema |
Targets live only in operator-static files (TARGET_MANIFEST in config.js) today; no centralized curation |
Decide: API-hosted targets vs. operator-static. Neither forces the other — flagged as an open question. |
| Variants / HpTF grouping |
❌ missing — measurements.label is one flat string per row; no grouping primitive |
Multi-pad / multi-position HpTF envelopes can't round-trip through the API |
|
Per-model display fields (price, description, image_url) |
❌ missing — models has only id, brand_id, name, timestamps |
Other metadata like price and description have nowhere to live |
|
| Shop-link / sponsor content |
❌ missing — no sponsor/shop-link entity; content lives as static JSON + runtime-eval()'d JS at squig.link/shoplinks.{js,json} and squig.link/squiglink-intro.js |
lab-next cannot fetch sponsor content without continuing to eval() remote scripts |
Would be better to migrate sponsor/shop-link content to a real API resource. |
| Welcome / intro content (first-visit modal) |
❌ missing — same as sponsor content; currently a squig.link-hosted JS script |
No per-tenant override channel |
Either a global GET /welcome-content endpoint or integrated welcome content in lab-next. |
| Theming presets per tenant (palette / logo / watermark / font) |
❌ missing — no tenant-settings concept; themes are operator-edited CSS files today |
Multi-tenant theming requirement has no backend |
New tenant_settings table or JSONB column on users/databases? |
§2a Algorithmic differences
Two features share names between lab and mGT but compute different things under the hood. The comparison tables reduce these to "both ✅" — this section makes the difference explicit so lab-next doesn't accidentally inherit one, the other, or an uncanny mix.
Normalization
lab has two modes:
- Hz-target — shifts every curve so its response at a chosen frequency (default 500 Hz per IEC) equals a fixed dB (default 60). Simple vertical shift.
- dB-target (loudness) — applies ISO 226:2003 equal-loudness weighting with free-field compensation, averages the weighted SPL to get a per-curve "loudness," then shifts to target that loudness. The weighting curve itself varies with the target dB (peaks ~700 Hz at low volumes, shifts to ~200 Hz at high volumes).
mGT has two modes and deliberately omits dB-target:
- Hz-target — same as lab.
- Avg — unweighted average over 300–3000 Hz (midrange).
Why dB-target was dropped in mGT? Raw FR measurements are typically conducted at a standardized calibration (94 dB SPL @ 500 Hz for example). When a user loads a measurement, the reasonable expectation is to see curves that preserve that calibration — not curves post-hoc shifted to equalize some subjective "loudness" across headphones.
Smoothing
lab. A cubic smoothing spline with frequency-weighted smoothness — bass is smoothed more heavily than treble, using a manually-tuned weighting that shifts the accuracy/smoothness tradeoff across frequency. It's a mathematically exact minimum of a specific weighted sum, and the smoothing parameter (default 5, range 1–10) controls only how heavily smoothness is weighted relative to accuracy. With smoothing = 0, lab falls back to a cardinal spline (not a natural cubic, which would overshoot).
mGT. Octave-fraction presets (1/48, 1/24, 1/12, 1/6, 1/3) — the industry convention in measurement software (REW, ARTA). Each preset defines a smoothing window width in logarithmic frequency space; treble and bass get the same octave-fraction treatment. No frequency-dependent weighting.
§3 Open questions for design discussion
-
Delivery model for squig.link services. Three options now exist: remote-script injection (lab's current centralized model — hot-updatable, no rebuild needed for content/sponsor changes), bundled in-app code (mGT's model — type-safe, testable, changes require a release), or Squiglink API endpoints for sponsor / shop-link / welcome / opt-out / analytics config (type-safe and hot-updatable via DB writes, at the cost of endpoint design and tenant-settings schema).
-
API-backed vs. JSON-driven lab-next. Does lab-next consume the Squiglink API as its primary data source (one tenant = one databases.user_id), or does it continue to fetch decentralized phone_book.json files per operator (with the API as just one source among many)? The answer shapes how much of the API we need to build before lab-next can launch, and how much of the legacy data model we need to preserve vs. migrate.
-
Where does the tenant-config layer live? configs, theming presets, opt-out flags, analytics IDs — do they become columns on users, columns on databases, a new tenant_settings table, JSONB blobs, or a separate config service?
Feature Comparison & Design-Phase Scoping
Why lab-next (should) start from modernGraphTool
Two starting points were on the table: lab — the long-running CrinGraph fork that still powers most squig.link sites and is familiar to every returning visitor — or modernGraphTool, the ground-up rewrite I've invested in over the past year. I think we should build lab-next on top of modernGraphTool for two reasons:
§1 Scope of comparison
pink-eqbranch — latest, introduces graphic-EQ presets, revised auto-EQ constraints, live music-spectrum overlaymaingraphtool.jsnpm run build→ staticdist/or CDNdist-cdn/squig.link(squigsites.js,shoplinks.js,squiglink-intro.js) — each operator's lab page loads them via<script src="https://squig.link/...">tags and the scripts attach themselves to the DOM at runtimesquiglink-store§2 Feature comparison tables
2.1 Graph display & interaction
GraphInspection.tsoverlayGraphHandle.tsdrag handleslabelsPosition,stickyLabelswatermark(svg)functionWATERMARKconfig arrayrig_descriptionRIG_DESCRIPTIONsaveSvgAsPng.jsScreenshotButtonalt_animated(toggleable)prefers-reduced-motion.alt_sticky_graphASPECT_RATIOconfig with bothpink-eq(graphtool.js:1148 —music-spectrum-viz,analyser)GraphSpectrumOverlayPreferenceBoundfeaturealt_tutorialwith labeled regionsFrequencyTutorialfeature2.2 Selection (phones, brands, targets)
PhoneSelector+ brand panelslistAugment.jsfile:["A","B"]+ suffix/prefixtargetswith type bucketsTARGET_MANIFESTwith i18nTargetCustomizertargetRestoreLastUsedsquigsites.js(registry select in header)CrossSiteSearchResults(live JSON fetch)2.3 Curve manipulation
default_normalization: "Hz"withdefault_norm_hz(default 500 Hz)NORMALIZATION.TYPE: "Hz"default_normalization: "dB"with ISO 226 + free-field weightingNORMALIZATION.TYPE: "Avg"scale_smoothingSampleChannelSelectornum_samplesin config.jssamplesin phone_book.jsonnum_samplesequivalent data from phone_book.json. This change was made to accomodate various devices with various amount of samples. Might be able to add defaultnum_samplesin config.js for compatibility.max_channel_imbalance(Kadane's algo)|L−R|in real dB. I personally think imbalance indicator isn't that useful though.GraphColorPickerpopover2.4 Equalizer (parametric / graphic)
EqFilterList+EqFilterCardEqAutoEq+EqAutoEqSelecteq-store.txtbiquad / Wavelet)EqAudioPlayerlinkEqNormalizationflag2.5 Audio playback (non-EQ)
extraPinkNoiseEnabledextraToneGeneratorEnabledextraMusicEnabledEqAudioPlayerpink-eqGraphSpectrumOverlay2.6 Sharing & persistence
?share=Phone1,Phone2(query string)init_phonesdefault loadINITIAL_PHONES/INITIAL_TARGETSshare_urlURL.AUTO_UPDATE_URLsaveSvgAsPng.jsScreenshotButtonsettings-storewithlocal/sessionmodes2.7 Theming
themingEnabled(stylesheet swap:dark.css/white.css).darkclass on<html>+ OKLCH tokenstheme.css(but still self-host only)targetDashed,targetColorCustomTRACE_STYLINGstickyLabels@theme2.8 squig.link integration
Delivery model is itself a comparison axis. There are now three of them, not two:
*.jsscripts hosted onsquig.link, pulled by every operator'sindex.htmland attached to the DOM at runtime. Hot-updatable across the whole network with no rebuild.squiglink-store. Type-safe and testable; changes require a release.server. Capabilities become REST resources rather than runtime-injected scripts or bundled client code. Today the API covers the data half (brands, models, measurements, evaluations) viaGET /brands,GET /models,GET /legacy/data/phone_book.json, etc., but has no endpoints for the integration-service half (sponsor/shop-link, welcome content, theming, cross-database search).Each row below calls out which of the three supports it today.
squigsites.jsreadssquigsites.json, builds a<select>grouped into 5128 / IEMs / Headphones / Earbuds / Official, injected intoul.header-linksCrossSiteSearchResultsfetchessquigsites.jsondirectly, renders nativeGET /brands,GET /modelsspan all databases but there's no unified "devices across all operators" endpoint; cross-site metadata (site name, URL shape, dbType) isn't modeledcommon/shoplinks.js—MutationObserveron#fr-graph, looks up active phone inshoplinks.json, fetches live per-product JSON from sponsor for current pricing, inlinedata:image/svg+xmlsponsor logo, UTM-tagged linksShopLinkfeatureeval()'d JS onsquig.linktodaycommon/squiglink-intro.js— 6+ slides with MP4 animations, embed-mode awareness,squig.link-domain cookie scopeSponsorBanner+TutorialModal2.9 i18n
en,ko)resolveI18nValue()ENABLE_SYSTEM_LANG_DETECTION2.10 Data & config
phone_book.jsonformatfile/suffix/prefixtsvParseflexibilitydefault_channels/num_samplestargetsarrayTARGET_MANIFESTwith i18n labelsconfig.jssurface2.11 Squiglink API coverage for lab-next features
This table audits the WIP Squiglink API (
server) against the feature-level decisions above. It is scoped to feature/endpoint-level asks.GET /brands,GET /modelsboth paginated and trigram-FTS searchablelimit/offsetquery paramspage_sizelater.GET /legacy/data/phone_book.json?database_id=Xemits CrinGraph-shape JSON;GET /legacy/data/{uuid} (L|R).txtreturns channel CSVdatabase_idsilently returns[]instead of 404/brandsand/modelsalready span all databases (brands/models are global), but there's no endpoint that returns "every measurement matching this query, with source operator + database metadata attached"/search/devicesthat joins brands × models × databases × users; no way to filter byDatabaseKindacross sitesdatabases.user_id,users.username,databases.kind,databases.pathto reconstruct today'ssquigsites.json-based UX.targetsentity in the schemaTARGET_MANIFESTinconfig.js) today; no centralized curationmeasurements.labelis one flat string per row; no grouping primitiveprice,description,image_url)modelshas onlyid,brand_id,name, timestampspriceanddescriptionhave nowhere to liveeval()'d JS atsquig.link/shoplinks.{js,json}andsquig.link/squiglink-intro.jseval()remote scriptsGET /welcome-contentendpoint or integrated welcome content in lab-next.tenant_settingstable or JSONB column onusers/databases?§2a Algorithmic differences
Two features share names between lab and mGT but compute different things under the hood. The comparison tables reduce these to "both ✅" — this section makes the difference explicit so lab-next doesn't accidentally inherit one, the other, or an uncanny mix.
Normalization
lab has two modes:
mGT has two modes and deliberately omits dB-target:
Why dB-target was dropped in mGT? Raw FR measurements are typically conducted at a standardized calibration (94 dB SPL @ 500 Hz for example). When a user loads a measurement, the reasonable expectation is to see curves that preserve that calibration — not curves post-hoc shifted to equalize some subjective "loudness" across headphones.
Smoothing
lab. A cubic smoothing spline with frequency-weighted smoothness — bass is smoothed more heavily than treble, using a manually-tuned weighting that shifts the accuracy/smoothness tradeoff across frequency. It's a mathematically exact minimum of a specific weighted sum, and the smoothing parameter (default 5, range 1–10) controls only how heavily smoothness is weighted relative to accuracy. With smoothing = 0, lab falls back to a cardinal spline (not a natural cubic, which would overshoot).
mGT. Octave-fraction presets (1/48, 1/24, 1/12, 1/6, 1/3) — the industry convention in measurement software (REW, ARTA). Each preset defines a smoothing window width in logarithmic frequency space; treble and bass get the same octave-fraction treatment. No frequency-dependent weighting.
§3 Open questions for design discussion
Delivery model for squig.link services. Three options now exist: remote-script injection (lab's current centralized model — hot-updatable, no rebuild needed for content/sponsor changes), bundled in-app code (mGT's model — type-safe, testable, changes require a release), or Squiglink API endpoints for sponsor / shop-link / welcome / opt-out / analytics config (type-safe and hot-updatable via DB writes, at the cost of endpoint design and tenant-settings schema).
API-backed vs. JSON-driven lab-next. Does lab-next consume the Squiglink API as its primary data source (one tenant = one
databases.user_id), or does it continue to fetch decentralizedphone_book.jsonfiles per operator (with the API as just one source among many)? The answer shapes how much of the API we need to build before lab-next can launch, and how much of the legacy data model we need to preserve vs. migrate.Where does the tenant-config layer live? configs, theming presets, opt-out flags, analytics IDs — do they become columns on
users, columns ondatabases, a newtenant_settingstable, JSONB blobs, or a separate config service?