Skip to content

Commit fafd64e

Browse files
wmwolfclaude
andcommitted
Improve pan interaction for logarithmic axes and add mode switching scripts
- Fix logarithmic axis panning using working-space approach for accurate calculations - Add Python scripts for easy development/production mode switching (switch-to-dev.py, switch-to-prod.py) - Update CLAUDE.md with mode switching instructions - Remove unreliable shell scripts in favor of Python implementation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 053e0c2 commit fafd64e

4 files changed

Lines changed: 231 additions & 31 deletions

File tree

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ This is a client-side web application with no build process. To develop locally:
8282
2. Use browser developer tools for debugging
8383
3. Files can be edited directly and refreshed in browser
8484

85+
### Development vs Production Mode Switching
86+
87+
Use these scripts to quickly switch between development and production JavaScript paths:
88+
89+
**Switch to Development Mode:**
90+
```bash
91+
python3 switch-to-dev.py
92+
```
93+
94+
**Switch to Production Mode:**
95+
```bash
96+
python3 switch-to-prod.py
97+
```
98+
99+
**IMPORTANT**: Always switch to production mode before committing changes to ensure production paths are active in the repository.
100+
85101
For generating column metadata (requires MESA installation):
86102

87103
```bash

js/interaction-manager.js

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -295,63 +295,91 @@ const interaction_manager = {
295295

296296
// Execute pan operation and update axis limits
297297
execute_pan: () => {
298-
// Calculate visual movement for consistent dual y-axis behavior
298+
const pixelDx = interaction_manager.interaction.drag_end.x - interaction_manager.interaction.drag_start.x;
299299
const pixelDy = interaction_manager.interaction.drag_end.y - interaction_manager.interaction.drag_start.y;
300300

301-
// Calculate data coordinate changes
301+
// Calculate data coordinate changes for each axis
302302
Object.keys(vis.axes).forEach(axis => {
303303
if (vis.axes[axis].scale && vis.axes[axis].data_name) {
304+
// Get current limits
305+
const currentMin = vis.axes[axis].min || vis.axes[axis].scale.domain()[0];
306+
const currentMax = vis.axes[axis].max || vis.axes[axis].scale.domain()[1];
307+
308+
// Validate current limits
309+
if (!isFinite(currentMin) || !isFinite(currentMax) || isNaN(currentMin) || isNaN(currentMax)) {
310+
console.warn(`Invalid current limits for axis ${axis}:`, currentMin, currentMax);
311+
return;
312+
}
313+
314+
const isLogScale = vis.axes[axis].type === 'log';
315+
let workingMin, workingMax;
316+
317+
// Step 1: Convert to working space (log space if logarithmic, regular space if linear)
318+
if (isLogScale) {
319+
if (currentMin <= 0 || currentMax <= 0) {
320+
console.warn(`Log axis ${axis} has non-positive limits:`, currentMin, currentMax);
321+
return;
322+
}
323+
workingMin = Math.log10(currentMin);
324+
workingMax = Math.log10(currentMax);
325+
} else {
326+
workingMin = currentMin;
327+
workingMax = currentMax;
328+
}
329+
330+
// Step 2: Calculate pixel-to-data conversion using working space limits
304331
let deltaData;
305332
if (axis === 'x') {
306-
deltaData = vis.axes[axis].scale.invert(interaction_manager.interaction.drag_start.x) -
307-
vis.axes[axis].scale.invert(interaction_manager.interaction.drag_end.x);
333+
// For x-axis, use horizontal pixel movement
334+
const plotWidth = vis.axes[axis].scale.range()[1] - vis.axes[axis].scale.range()[0];
335+
const workingRange = workingMax - workingMin;
336+
deltaData = -(pixelDx / plotWidth) * workingRange; // Negative because right drag should move data left
308337
} else {
309-
// For y-axes, calculate the visual percentage movement and apply to each axis's range
310-
const currentMin = vis.axes[axis].min || vis.axes[axis].scale.domain()[0];
311-
const currentMax = vis.axes[axis].max || vis.axes[axis].scale.domain()[1];
312-
const currentRange = currentMax - currentMin;
313-
314-
// Calculate what percentage of the visual height we moved
338+
// For y-axes, use vertical pixel movement
315339
const plotHeight = vis.axes[axis].scale.range()[0] - vis.axes[axis].scale.range()[1]; // range is [bottom, top]
316-
const visualPercent = pixelDy / plotHeight; // SVG coordinate to data coordinate mapping
317-
318-
// Apply this percentage to the current data range
319-
deltaData = visualPercent * currentRange;
340+
const workingRange = workingMax - workingMin;
341+
deltaData = (pixelDy / plotHeight) * workingRange; // Positive because down drag should move data down
320342
}
321343

322-
// Validate deltaData to prevent invalid values
344+
// Validate deltaData
323345
if (!isFinite(deltaData) || isNaN(deltaData)) {
324346
console.warn(`Invalid deltaData for axis ${axis}:`, deltaData);
325347
return;
326348
}
327349

328-
// Update axis limits
329-
const currentMin = vis.axes[axis].min || vis.axes[axis].scale.domain()[0];
330-
const currentMax = vis.axes[axis].max || vis.axes[axis].scale.domain()[1];
350+
// Step 3: Apply the change in working space
351+
const newWorkingMin = workingMin + deltaData;
352+
const newWorkingMax = workingMax + deltaData;
331353

332-
// Validate current limits
333-
if (!isFinite(currentMin) || !isFinite(currentMax) || isNaN(currentMin) || isNaN(currentMax)) {
334-
console.warn(`Invalid current limits for axis ${axis}:`, currentMin, currentMax);
354+
// Validate new working limits
355+
if (!isFinite(newWorkingMin) || !isFinite(newWorkingMax) || isNaN(newWorkingMin) || isNaN(newWorkingMax)) {
356+
console.warn(`Invalid new working limits for axis ${axis}:`, newWorkingMin, newWorkingMax);
335357
return;
336358
}
337359

338-
const newMin = currentMin + deltaData;
339-
const newMax = currentMax + deltaData;
340-
341-
// Special validation for logarithmic axes
342-
if (vis.axes[axis].type === 'log') {
343-
if (newMin <= 0 || newMax <= 0) {
344-
console.warn(`Log axis ${axis} pan would create non-positive limits:`, newMin, newMax);
345-
return; // Skip this axis - can't have non-positive values on log scale
360+
// Step 4: Convert back to real space
361+
let newMin, newMax;
362+
if (isLogScale) {
363+
newMin = Math.pow(10, newWorkingMin);
364+
newMax = Math.pow(10, newWorkingMax);
365+
366+
// Additional validation for log scale
367+
if (newMin <= 0 || newMax <= 0 || !isFinite(newMin) || !isFinite(newMax)) {
368+
console.warn(`Log axis ${axis} pan would create invalid limits:`, newMin, newMax);
369+
return;
346370
}
371+
} else {
372+
newMin = newWorkingMin;
373+
newMax = newWorkingMax;
347374
}
348375

349-
// Validate new limits
376+
// Final validation
350377
if (!isFinite(newMin) || !isFinite(newMax) || isNaN(newMin) || isNaN(newMax)) {
351-
console.warn(`Invalid new limits for axis ${axis}:`, newMin, newMax);
378+
console.warn(`Invalid final limits for axis ${axis}:`, newMin, newMax);
352379
return;
353380
}
354381

382+
// Step 5: Update axis limits
355383
vis.axes[axis].min = newMin;
356384
vis.axes[axis].max = newMax;
357385

switch-to-dev.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python3
2+
"""Switch to development mode by enabling dev scripts and disabling production scripts."""
3+
4+
import re
5+
6+
def switch_to_dev():
7+
with open('index.html', 'r') as f:
8+
content = f.read()
9+
10+
# Switch color-modes script
11+
# Comment out production color-modes if it's active
12+
content = re.sub(
13+
r'^\s*<script src="/mesa-explorer/js/color-modes\.js"></script>',
14+
r'\t<!-- <script src="/mesa-explorer/js/color-modes.js"></script> -->',
15+
content,
16+
flags=re.MULTILINE
17+
)
18+
# Uncomment development color-modes if it's commented
19+
content = re.sub(
20+
r'^\s*<!-- <script src="/js/color-modes\.js"></script> -->',
21+
r'\t<script src="/js/color-modes.js"></script>',
22+
content,
23+
flags=re.MULTILINE
24+
)
25+
26+
# Handle production scripts - comment them out if they're active
27+
production_scripts = [
28+
'file-manager.js', 'ui-utils.js', 'style-manager.js', 'metadata-manager.js',
29+
'text-markup.js', 'data-utils.js', 'series-manager.js', 'interaction-manager.js',
30+
'download-manager.js', 'controls-manager.js', 'mesa-explorer.js'
31+
]
32+
33+
for script in production_scripts:
34+
content = re.sub(
35+
rf'^\s*<script src="/mesa-explorer/js/{script}"></script>',
36+
rf'\t\t<!-- <script src="/mesa-explorer/js/{script}"></script> -->',
37+
content,
38+
flags=re.MULTILINE
39+
)
40+
41+
# Uncomment development scripts that are currently commented
42+
# Handle the multi-line comment block for core utilities
43+
content = re.sub(
44+
r'<!-- <script src="/js/file-manager\.js"></script>\s*\n\s*<script src="/js/ui-utils\.js"></script>\s*\n\s*<script src="/js/style-manager\.js"></script>\s*\n\s*<script src="/js/metadata-manager\.js"></script>\s*\n\s*<script src="/js/text-markup\.js"></script> -->',
45+
r'<script src="/js/file-manager.js"></script>\n\t\t<script src="/js/ui-utils.js"></script>\n\t\t<script src="/js/style-manager.js"></script>\n\t\t<script src="/js/metadata-manager.js"></script>\n\t\t<script src="/js/text-markup.js"></script>',
46+
content
47+
)
48+
49+
# Handle data-utils
50+
content = re.sub(
51+
r'^\s*<!-- <script src="/js/data-utils\.js"></script> -->',
52+
r'\t\t<script src="/js/data-utils.js"></script>',
53+
content,
54+
flags=re.MULTILINE
55+
)
56+
57+
# Handle the UI management multi-line comment
58+
content = re.sub(
59+
r'<!-- <script src="/js/series-manager\.js"></script>\s*\n\s*<script src="/js/interaction-manager\.js"></script>\s*\n\s*<script src="/js/download-manager\.js"></script>\s*\n\s*<script src="/js/controls-manager\.js"></script> -->',
60+
r'<script src="/js/series-manager.js"></script>\n\t\t<script src="/js/interaction-manager.js"></script>\n\t\t<script src="/js/download-manager.js"></script>\n\t\t<script src="/js/controls-manager.js"></script>',
61+
content
62+
)
63+
64+
# Handle mesa-explorer
65+
content = re.sub(
66+
r'^\s*<!-- <script src="/js/mesa-explorer\.js"></script> -->',
67+
r'\t\t<script src="/js/mesa-explorer.js"></script>',
68+
content,
69+
flags=re.MULTILINE
70+
)
71+
72+
with open('index.html', 'w') as f:
73+
f.write(content)
74+
75+
print("Switched to development mode. Development scripts are now active.")
76+
77+
if __name__ == "__main__":
78+
switch_to_dev()

switch-to-prod.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python3
2+
"""Switch to production mode by enabling production scripts and disabling dev scripts."""
3+
4+
import re
5+
6+
def switch_to_prod():
7+
with open('index.html', 'r') as f:
8+
content = f.read()
9+
10+
# Switch color-modes script
11+
# Comment out development color-modes if it's active
12+
content = re.sub(
13+
r'^\s*<script src="/js/color-modes\.js"></script>',
14+
r'\t<!-- <script src="/js/color-modes.js"></script> -->',
15+
content,
16+
flags=re.MULTILINE
17+
)
18+
# Uncomment production color-modes if it's commented
19+
content = re.sub(
20+
r'^\s*<!-- <script src="/mesa-explorer/js/color-modes\.js"></script> -->',
21+
r'\t<script src="/mesa-explorer/js/color-modes.js"></script>',
22+
content,
23+
flags=re.MULTILINE
24+
)
25+
26+
# Handle development scripts - comment them out if they're active
27+
# First, comment out individual active development scripts
28+
dev_scripts = [
29+
'file-manager.js', 'ui-utils.js', 'style-manager.js', 'metadata-manager.js',
30+
'text-markup.js', 'data-utils.js', 'series-manager.js', 'interaction-manager.js',
31+
'download-manager.js', 'controls-manager.js', 'mesa-explorer.js'
32+
]
33+
34+
for script in dev_scripts:
35+
content = re.sub(
36+
rf'^\s*<script src="/js/{script}"></script>',
37+
rf'\t\t<!-- <script src="/js/{script}"></script> -->',
38+
content,
39+
flags=re.MULTILINE
40+
)
41+
42+
# Now group them back into multi-line comments as in the original structure
43+
# Group core utilities
44+
content = re.sub(
45+
r'(\t\t)<!-- <script src="/js/file-manager\.js"></script> -->\s*\n\s*<!-- <script src="/js/ui-utils\.js"></script> -->\s*\n\s*<!-- <script src="/js/style-manager\.js"></script> -->\s*\n\s*<!-- <script src="/js/metadata-manager\.js"></script> -->\s*\n\s*<!-- <script src="/js/text-markup\.js"></script> -->',
46+
r'\1<!-- <script src="/js/file-manager.js"></script>\n\t\t<script src="/js/ui-utils.js"></script>\n\t\t<script src="/js/style-manager.js"></script>\n\t\t<script src="/js/metadata-manager.js"></script>\n\t\t<script src="/js/text-markup.js"></script> -->',
47+
content
48+
)
49+
50+
# Group UI management scripts
51+
content = re.sub(
52+
r'(\t\t)<!-- <script src="/js/series-manager\.js"></script> -->\s*\n\s*<!-- <script src="/js/interaction-manager\.js"></script> -->\s*\n\s*<!-- <script src="/js/download-manager\.js"></script> -->\s*\n\s*<!-- <script src="/js/controls-manager\.js"></script> -->',
53+
r'\1<!-- <script src="/js/series-manager.js"></script>\n\t\t<script src="/js/interaction-manager.js"></script>\n\t\t<script src="/js/download-manager.js"></script>\n\t\t<script src="/js/controls-manager.js"></script> -->',
54+
content
55+
)
56+
57+
# Uncomment production scripts if they're commented
58+
production_scripts = [
59+
'file-manager.js', 'ui-utils.js', 'style-manager.js', 'metadata-manager.js',
60+
'text-markup.js', 'data-utils.js', 'series-manager.js', 'interaction-manager.js',
61+
'download-manager.js', 'controls-manager.js', 'mesa-explorer.js'
62+
]
63+
64+
for script in production_scripts:
65+
content = re.sub(
66+
rf'^\s*<!-- <script src="/mesa-explorer/js/{script}"></script> -->',
67+
rf'\t\t<script src="/mesa-explorer/js/{script}"></script>',
68+
content,
69+
flags=re.MULTILINE
70+
)
71+
72+
with open('index.html', 'w') as f:
73+
f.write(content)
74+
75+
print("Switched to production mode. Production scripts are now active.")
76+
77+
if __name__ == "__main__":
78+
switch_to_prod()

0 commit comments

Comments
 (0)