Last updated: 2026-03-26
This guide helps developers diagnose and fix rendering issues in GramFrame — mispositioned elements, missing overlays, coordinate mismatches, and SVG problems.
Understanding which files handle what is the first step in narrowing down a rendering bug:
| Subsystem | File | Handles |
|---|---|---|
| Render entry point | src/rendering/cursors.js |
updateCursorIndicators() — clears and redraws all features |
| Feature coordination | src/core/FeatureRenderer.js |
Cross-mode visibility; delegates to each mode's renderer |
| SVG element creation | src/utils/svg.js |
createSVGLine, createSVGText, createSVGCircle |
| Coordinate transforms | src/utils/coordinates.js |
screenToSVGCoordinates, imageToDataCoordinates |
| Zoom-aware conversion | src/core/events.js |
screenToDataWithZoom() — full pipeline with zoom |
| SVG layout and axes | src/components/table.js |
updateSVGLayout(), renderAxes(), applyZoomTransform() |
| Viewport operations | src/core/viewport.js |
handleResize(), updateAxes(), zoom operations |
| Mode-specific rendering | src/modes/*/ |
Each mode's renderPersistentFeatures() and renderCursor() |
| Event triggering | src/core/events.js |
Mouse handlers that trigger re-renders |
Every mouse event or mode switch triggers this sequence:
Mouse event / mode switch
│
▼
updateCursorIndicators(instance)
│
├─ cursorGroup.innerHTML = '' (clear all SVG features)
│
└─ featureRenderer.renderAllPersistentFeatures()
│
├─ analysis mode → renderPersistentFeatures() (markers)
├─ harmonics mode → renderPersistentFeatures() (harmonic sets)
└─ doppler mode → renderPersistentFeatures() (curves/markers)
Symptom: Elements appear in the wrong position — shifted, stretched, or mirrored.
Likely causes:
-
Forgetting the Y-axis inversion: In SVG, Y=0 is at the top. In data space, time=0 is at the bottom. The conversion uses
(1 - timeRatio):svgY = margins.top + (1 - timeRatio) * naturalHeight
If you see elements vertically flipped, check for a missing
1 -in the Y calculation. -
Not accounting for margins: SVG elements are positioned relative to the viewBox, which includes margin space. Image coordinates start at
(margins.left, margins.top), not(0, 0). -
Rate not applied: Frequency values are divided by
rate. If you're computing frequency positions, ensure you use the rate-adjusted value fromimageToDataCoordinates, or apply/ rateyourself. -
Zoom not accounted for: When zoomed, the image position and dimensions change.
screenToDataWithZoom()inevents.jshandles this, but if you're doing manual coordinate math, you need to account for the zoomed image'sx,y,width, andheightattributes.
Symptom: Elements are clipped, invisible, or at wrong scale.
Likely causes:
- ViewBox mismatch: The SVG viewBox should match the image's natural dimensions plus margins. If the viewBox is wrong, all coordinate math breaks.
- Elements outside viewBox: Features rendered outside the viewBox bounds are clipped. Check that computed SVG coordinates fall within the expected range.
Symptom: Elements are correct initially but wrong after resize, or elements briefly appear in wrong positions.
Likely causes:
- Missing ResizeObserver handling:
setupResizeObserver()triggers_handleResize()when the container changes size. If your feature caches positions, they need to be recalculated on resize. - Stale cached dimensions: If you read
getBoundingClientRect()once and cache it, the values will be wrong after a resize. Always read dimensions fresh or listen for resize events.
Symptom: Feature appears to lag, shows stale data, or doesn't update.
Likely causes:
- State modified but
notifyStateListenersnot called: After changing state, you must callnotifyStateListeners(instance.state, instance.stateListeners)to broadcast the change. - State modified after render: If state is updated after
updateCursorIndicatorsruns, the rendered SVG won't reflect the new state until the next render cycle. - Deep-copy timing: Listeners receive a deep copy of state. If a listener triggers further state changes, those happen on the copy, not on the real state.
Symptom: A feature renders in its own mode but disappears when switching to another mode.
Fix: Register the feature check in FeatureRenderer:
- Add a
hasMyFeatures()method - Call your mode's
renderPersistentFeatures()inrenderAllPersistentFeatures()
See ADR-011.
- Right-click on the spectrogram → Inspect Element
- Navigate to the
<svg>element in the Elements panel - Expand
<g>groups — look forcursorGroupcontaining your features - Hover over SVG elements to see their bounding boxes highlighted
- Check element attributes (
x,y,cx,cy,x1,y1, etc.) against expected values
The debug.html page (served at /debug.html during yarn dev) provides:
- A live state display panel showing the full JSON state
- Real-time updates as you interact with the spectrogram
- Useful for verifying that state changes are happening correctly
To check a rendering issue:
- Open
debug.htmlin the browser - Interact with the spectrogram
- Watch the state JSON update — verify
cursorPosition,markers, etc. have expected values - If state is correct but rendering is wrong → the bug is in rendering code
- If state is wrong → the bug is in event handling or mode logic
Add temporary logging to trace coordinate transforms:
// In events.js → screenToDataWithZoom():
console.log('Screen:', { screenX, screenY })
console.log('SVG:', svgCoords)
console.log('Image:', { imageX, imageY })
console.log('Data:', dataCoords)This shows exactly where in the pipeline a coordinate goes wrong.
In the browser console:
const svg = document.querySelector('.gram-frame-container svg')
console.log('viewBox:', svg.getAttribute('viewBox'))
console.log('bounding rect:', svg.getBoundingClientRect())Compare the viewBox dimensions with the image's natural dimensions + margins.
These are the default margins (from state.margins):
| Margin | Value | Purpose |
|---|---|---|
left |
60px | Space for time axis labels |
bottom |
50px | Space for frequency axis labels |
right |
15px | Small right padding |
top |
15px | Small top padding |
All SVG feature positions must account for these offsets.
Feature renders in wrong position?
│
├─ Check: Are coordinates correct in state? (use debug.html)
│ ├─ No → Bug in event handling (src/core/events.js)
│ └─ Yes → Bug in rendering (src/rendering/ or mode's render method)
│
├─ Check: Is Y-axis inverted?
│ └─ Missing (1 - timeRatio) in Y calculation
│
├─ Check: Are margins included?
│ └─ Missing margins.left / margins.top offset
│
└─ Check: Is zoom active?
└─ Not using zoomed image dimensions from spectrogramImage attributes
- Tech-Architecture.md — Full system architecture
- Adding-Graphical-Features.md — Guide to adding new visual features
- Component-Strategy.md — Component resize and lifecycle strategy
- ADR-001: SVG-Based Rendering
- ADR-002: Multiple Coordinate Systems