Skip to content

Feature Comparison & Design-Phase Scoping #1

@potatosalad775

Description

@potatosalad775

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:1148music-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 ⚠️ partialGET /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.jsMutationObserver 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

  1. 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).

  2. 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.

  3. 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions