Skip to content

Adaptive mel-scale y-axis grid lines for finer low-frequency granularity#598

Merged
atruskie merged 7 commits intomainfrom
copilot/add-denser-gridlines-for-mel-scale
May 6, 2026
Merged

Adaptive mel-scale y-axis grid lines for finer low-frequency granularity#598
atruskie merged 7 commits intomainfrom
copilot/add-denser-gridlines-for-mel-scale

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

Mel-scale spectrograms were rendering y-axis grid lines at fixed 1 kHz intervals regardless of scale — leaving the visually dominant low-frequency region (which the mel scale expands) with almost no reference lines, while the compressed high-frequency region was relatively dense.

Root cause

calculateStep() derived the initial step from the mel-valued frequencyDomain midpoint, and willFitStep() only checked overlap at the highest-frequency pair (the most compressed). This correctly prevented overlap at the top but forced a 1 kHz step even though the expanded lower region had ample room for finer ticks.

Changes — src/components/axes/axes.ts

  • New generateMelScaleFrequencyValues() — two-pass adaptive tick generator:

    1. Fine step selection: walks [100, 200, 500, 1000, 2000, 5000, 10000] Hz and picks the smallest that produces ≥ minPixelSpacing separation at the low-frequency end of the scale. Steps are restricted to multiples of 100 Hz to keep 1-decimal-place kHz labels unambiguous.
    2. Generate → filter: emits all candidates at the fine step, then drops any whose canvas position is within minPixelSpacing of the previously kept tick. Because the mel scale compresses high frequencies, this naturally prunes the top end while preserving the dense low-frequency labels.
  • minPixelSpacing uses double the label height as its base (fontSize.height * 2 + fontSize.height * labelPadding * 2) to keep labels readable without crowding. This means a 500 px canvas selects a 200 Hz fine step, while a 1000 px canvas still fits a 100 Hz fine step.

  • yValues() updated — routes to generateMelScaleFrequencyValues() when mel scale is active and yStepOverride is not set; existing calculateStep / generateAxisValues path unchanged otherwise.

Typical result (500 px canvas, 0–11 kHz):

Tick count Example labels
Before 11 0.0, 1.0, 2.0 … 10.0 kHz
After ~15 0.0, 0.2, 0.4 … 1.0, 1.4 … 3.0, 3.6 … 10.x kHz

Step count and density auto-scale with canvas height — a 500 px canvas gets ~15 ticks at a 200 Hz fine step; a 1000 px canvas gets ~25 ticks at a 100 Hz fine step.

Changes — src/tests/axes-spectrogram/

  • axes-spectrogram.e2e.fixture.ts: added createWithMelScaleAndSize() and createWithMelScaleStepOverrideAndSize() fixture helpers.
  • axes-spectrogram.e2e.spec.ts: added mel-scale y-axis grid lines describe block with tests verifying:
    • The first y-axis label step is ≈ 0.2 kHz (200 Hz) for 500 px canvas heights and ≈ 0.1 kHz (100 Hz) for 1000 px canvas heights, confirming the adaptive algorithm selects a finer step than the 1 kHz linear default.
    • A minimum tick density is maintained at each tested canvas size.
    • Setting the y-step attribute on <oe-axes> still bypasses the adaptive algorithm and applies the user-specified step exactly, even when mel-scale is active.

Copilot AI linked an issue Apr 30, 2026 that may be closed by this pull request
Copilot AI and others added 2 commits April 30, 2026 02:02
…rithm

When mel scale is active and no step override is set, yValues() now calls
generateMelScaleFrequencyValues() which:
1. Finds the finest 'nice' Hz step (100, 200, 500... Hz) that provides
   adequate pixel spacing at the low-frequency end of the mel scale
2. Generates all candidate ticks at that step
3. Filters out candidates whose canvas positions are too close together,
   naturally pruning the compressed high-frequency end

Result: ~32 well-spaced ticks for a typical 600px/0-11kHz mel spectrogram
vs the previous 11 ticks at 1kHz intervals.

Agent-Logs-Url: https://github.com/ecoacoustics/web-components/sessions/2f19bd2a-f093-43c4-9f41-6168d03b8437

Co-authored-by: atruskie <1452968+atruskie@users.noreply.github.com>
Copilot AI changed the title [WIP] Add more granularity to mel scale grid lines Adaptive mel-scale y-axis grid lines for finer low-frequency granularity Apr 30, 2026
Copilot AI requested a review from atruskie April 30, 2026 02:03
Copy link
Copy Markdown
Contributor

@atruskie atruskie left a comment

Choose a reason for hiding this comment

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

@copilot:

Don't abbreviate.

Add some tests in for this new behaviour.

Show me a screenshot of it working at various sizes.

Comment thread src/components/axes/axes.ts Outdated

// Generate all candidate tick values at the fine step.
const candidates: Hertz[] = [];
for (let freq = lowFrequency; freq < highFrequency; freq += fineStep) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

don't abbreviate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7279635 — both loops now use candidateFrequency.

@atruskie atruskie marked this pull request as ready for review April 30, 2026 05:10
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 30, 2026

@copilot:

Don't abbreviate....

Done in 7279635:

Abbreviation: renamed freqcandidateFrequency in both loops in generateMelScaleFrequencyValues().

Tests added in axes-spectrogram.e2e.spec.ts (mel-scale y-axis grid lines describe block):

  • First y-axis step ≈ 0.1 kHz at 500×500, 1000×1000, and 1000×500 canvas sizes (shows the adaptive algorithm selects a 100 Hz fine step instead of 1 kHz)
  • Minimum tick density assertion for each size
  • y-step attribute override on <oe-axes> still bypasses the adaptive algorithm when mel-scale is active

Two new fixture methods were added: createWithMelScaleAndSize and createWithMelScaleStepOverrideAndSize.

Screenshots at various sizes:

Mel Scale Grid Lines at various sizes

  • Linear scale 500×400 (top-left): coarse 1 kHz labels throughout
  • Mel scale 500×400 (top-right): dense 0.1 kHz labels at low frequencies, coarsening toward the top
  • Mel scale 500×800 (bottom-left): tall canvas shows even more fine-grained labels at the low end
  • Mel scale 500×200 (bottom-right): short canvas adapts to 200 Hz fine step while keeping labels readable

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • fonts.gstatic.com
    • Triggering command: /home/REDACTED/.cache/ms-playwright/chromium_headless_shell-1179/chrome-linux/headless_shell /home/REDACTED/.cache/ms-playwright/chromium_headless_shell-1179/chrome-linux/headless_shell --disable-field-trial-config --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,Impro ash user.name (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from atruskie April 30, 2026 05:37
@atruskie
Copy link
Copy Markdown
Contributor

@copilot it looks a bit dense. Try doubling the height of the overlap check.

Produce spectrograms for mel and non mel variations at different sizes and show in your response in GitHub

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 30, 2026

@copilot it looks a bit dense. Try doubling the height of the overlap check.

Produce spectrograms for mel and non mel variations at different sizes and show in your response in GitHub

Done in 91dca73. Doubled the base label height in the overlap threshold:

// Before
const minPixelSpacing = fontSize.height + fontSize.height * AxesComponent.labelPadding * 2;
// After
const minPixelSpacing = fontSize.height * 2 + fontSize.height * AxesComponent.labelPadding * 2;

Effect on fine-step selection: a 500 px canvas no longer fits 100 Hz at the low-frequency end (~23.6 px gap < 27.5 px threshold), so it steps up to 200 Hz. A 1000 px canvas retains 100 Hz (~47 px gap > threshold).

Below is a side-by-side at four heights (linear left, mel right):

Mel vs Linear at 500×400, 500×600, 500×800, 500×200

Labels are now well-spaced and readable at all tested sizes. The mel-scale still carries significantly more low-frequency reference lines than the coarse 1 kHz linear grid.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • fonts.gstatic.com
    • Triggering command: /home/REDACTED/.cache/ms-playwright/chromium_headless_shell-1179/chrome-linux/headless_shell /home/REDACTED/.cache/ms-playwright/chromium_headless_shell-1179/chrome-linux/headless_shell --disable-field-trial-config --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,Impro dof$ (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Axes now generate steps progressively for mel-scales, jumping up magnitudes as we cross base 10 magnitude boundaries.

This results in much nicer automated mel scale axes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to improve y-axis tick/grid density when rendering spectrograms with mel scaling, so the expanded low-frequency region gets more useful reference labels while still preventing label overlap toward the compressed high-frequency end.

Changes:

  • Refactors axis tick generation in oe-axes to use stepper functions (basic + mel-aware) and updates label spacing/padding calculations.
  • Adds Playwright E2E fixture helpers and new tests covering mel-scale y-axis tick density and step behavior across canvas sizes.
  • Updates the local dev demo page interactions (event listeners + valid window-size) and bumps the pinned pnpm version.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/components/axes/axes.ts Refactors tick generation (stepper-based), adjusts label padding, and introduces mel-aware stepping logic.
src/tests/axes-spectrogram/axes-spectrogram.e2e.fixture.ts Adds fixture helpers to create mel-scale spectrograms and mel-scale + y-step override cases.
src/tests/axes-spectrogram/axes-spectrogram.e2e.spec.ts Adds mel-scale y-axis grid line tests and adjusts some size-based expectations.
dev/full-spectrogram.html Improves demo UI wiring (removes inline handlers, uses module script, fixes window-size).
package.json Updates the pinned packageManager pnpm version.

Comment on lines 397 to +407
const step =
this.yStepOverride ??
this.calculateStep(
this.unitConverter.frequencyDomain.value,
this.unitConverter.frequencyRange.value,
this.unitConverter.scaleY.value,
"height",
labelLength,
);

const stepper = this.unitConverter.melScale.value
? this.melStepper(step, labelLength, this.unitConverter.scaleY.value)
: this.basicStepper(step, labelLength, this.unitConverter.scaleY.value);
Comment on lines +405 to +407
const stepper = this.unitConverter.melScale.value
? this.melStepper(step, labelLength, this.unitConverter.scaleY.value)
: this.basicStepper(step, labelLength, this.unitConverter.scaleY.value);
Comment thread src/components/axes/axes.ts
Comment thread src/components/axes/axes.ts Outdated
@atruskie atruskie merged commit 428b6d4 into main May 6, 2026
32 checks passed
@atruskie atruskie deleted the copilot/add-denser-gridlines-for-mel-scale branch May 6, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mel scale grid lines - need more granualaity

3 participants