Modifying enabled axis ranges interactively
thomas_braun
I often plot multiple datasets against a common x-axis but each of them has its own y-axis.
Example:
Function dostuff()
Make/O data1 = p
Make/O data2 = -p
Display
AppendToGraph/L=ax1 data1
AppendToGraph/L=ax2 data2
ModifyGraph axisEnab(ax2)={0,0.475}, axisEnab(ax1)={0.525,1}, freePos(ax1)={0,bottom}, freePos(ax2)={0,bottom}
End
Make/O data1 = p
Make/O data2 = -p
Display
AppendToGraph/L=ax1 data1
AppendToGraph/L=ax2 data2
ModifyGraph axisEnab(ax2)={0,0.475}, axisEnab(ax1)={0.525,1}, freePos(ax1)={0,bottom}, freePos(ax2)={0,bottom}
End
This works without any problems and also looks nice. But now I have the request to allow the user to interactively change the enabled axis ranges with the mouse. Is that possible? IP9 only is okay.
How about a hook function where you react to a scroll event when the mouse is positioned in the left margin of the graph window. you could place some object (a button?) at the boundary that separates the two axes and have it enabled on mouseover, then shift the axis ranges and button position as the user scrolls.
January 7, 2022 at 08:31 am - Permalink
Maybe I don't get something here. Isn't that the default behavior of Igor? Hover with mouse cursor over the axis in question and scroll with the mouse to change the axis range. Just scroll to zoom or hold alt / option to shift ... or is click and drag desired?
January 7, 2022 at 08:43 am - Permalink
I think thomas wants to change the range over which the axis is drawn:
ModifyGraph axisEnab(ax2)={0,0.5}
January 7, 2022 at 09:01 am - Permalink
In IP9 you can make a transparent button that sits in the zone between the top and bottom plot areas.
On mouse enter, draw a horizontal line across the width of the graph as a visual cue and set a userdata flag to enable changing the ranges
A hook function checks the flag and changes the axis enabled ranges and the position of the horizontal line on scroll event
On mouse exit kill the dividing line, reposition the invisible button (taking a great deal of care with coordinates) and reset the userdata flag
edit: and the button has to be repositioned after window resize events.
January 7, 2022 at 09:11 am - Permalink
Wow so many replies so fast :)
Yes I want to change
axisEnab
and not the displayed axis range.
@tony:
Thanks for the idea, that seems doable. I'll report back.
January 7, 2022 at 09:25 am - Permalink
I have long thought there should be more mouse interaction with axis layout, but have many ideas and limited time.
January 7, 2022 at 09:42 am - Permalink
If that helps: The Global Fit Panel features a dragable divider line between the two listboxes using the mousedown, mousemoved and mouseup events. This works very nice and may serve as an inspiration for coding a similar dragable divider between the two axes, The code can be found in the panel's hook function starting from line 1732 inside Global Fit 2.ipf (this refers to the Igor 9 version; the particular code might be on a somewhat different line in the Igor 8 version but should be close).
January 7, 2022 at 09:57 am - Permalink
Here's a solution that the good folks at Wavemetrics helped out with several years ago which I use in our library of routines. It uses the marquee menu to zoom in on any axis of a graph.
"-"
"Refresh axis list", BuildMenu "GraphMarquee" //Build the Expand Sp menu on demand
Submenu "Expand Sp"
marqueeItem(0),/Q, ExpandSpecial(0)
marqueeItem(1),/Q, ExpandSpecial(1)
marqueeItem(2),/Q, ExpandSpecial(2)
marqueeItem(3),/Q, ExpandSpecial(3)
marqueeItem(4),/Q, ExpandSpecial(4)
marqueeItem(5),/Q, ExpandSpecial(5)
marqueeItem(6),/Q, ExpandSpecial(6)
marqueeItem(7),/Q, ExpandSpecial(7)
// as many as you think you'll ever have
End
end menu
function/S marqueeItem(index)
Variable index
String topGraph= WinName(0,1)
String menuStr="\\M0:(:_no more axes_" // \\M0 to make disabling work on Mac and Win
if( strlen(topGraph) ) //sometimes, command below produces list in different order.
String aList= HVAxisList(topGraph,0) // only left,right, etc. Only vertical axes.
if (FindListItem("right", aList, ";", 0)!=-1) //if this is in the list, pull it out of the list and make it 1st
aList = "right;"+RemoveFromList("right", aList, ";")
endif
if (FindListItem("left", aList, ";", 0)!=-1) //if this is in the list, pull it out of the list and make it 1st
aList = "left;"+RemoveFromList("left", aList, ";")
endif
String thisAxis= StringFromList(index,aList)
if( strlen(thisAxis) )
menuStr=thisAxis
endif
endif
return menuStr
End
Function ExpandSpecial(index)
Variable index
String topGraph= WinName(0,1)
if( strlen(topGraph) )
String aList= HVAxisList(topGraph,0) // only left,right, etc, not bottom
if (FindListItem("right", aList, ";", 0)!=-1) //if this is in the list, pull it out of the list and make it 1st
aList = "right;"+RemoveFromList("right", aList, ";")
endif
if (FindListItem("left", aList, ";", 0)!=-1) //if this is in the list, pull it out of the list and make it 1st
aList = "left;"+RemoveFromList("left", aList, ";")
endif
String thisAxis= StringFromList(index,aList)
if( strlen(thisAxis) )
GetMarquee/K $thisAxis
SetAxis/W=$topGraph $thisAxis, V_bottom, V_top
endif
endif
End
January 7, 2022 at 10:55 am - Permalink
To test it, make thomas' plot, then execute
if (!(s.eventCode==3 || s.eventCode==4))
return 0
endif
int status = str2num(GetUserData(s.winName, "", ""))
int buttondown, dy, pixel
string strInfo = AxisInfo(s.winName, "ax1")
strInfo = StringByKey("axisEnab(x)", strInfo, "=")
variable divider, pcdy, newdivider
sscanf strInfo, "{%f,", divider
divider -= 0.025
GetWindow $s.winName, psizeDC
int plotTop = v_top
int plotBottom = v_bottom
int plotHeight = v_bottom - v_top
variable pixDivider = v_top + (1-divider)*plotHeight
variable pixTop = plotTop + 0.1 * plotHeight
variable pixBottom = v_top + 0.9 * plotHeight
if (s.eventCode==4) // mousemoved
DrawAction /W=$s.winName getgroup=theline, delete
if (abs(s.mouseloc.v-pixDivider) < 5)
SetDrawEnv linefgc=(0x2222,0x2222,0x2222), dash=1
SetDrawEnv gstart, gname=theline
DrawLine -0.2, 1-divider, 1.2, 1-divider
SetDrawEnv /W=$s.winName gstop, gname=theline
SetWindow $s.winName userdata="1"
s.cursorCode = 6
s.doSetCursor = 1
else
SetWindow $s.winName userdata="0"
endif
DoUpdate /W=$s.winName
elseif (status==1 && s.eventCode==3) // on target, mousedown
// Hook function is not reentrant,
// i.e. mousemoved events are not generated while the loop is running
s.mouseLoc.v = limit(s.mouseLoc.v, pixTop, pixBottom)
do
GetMouse /W=$s.winName
buttondown = V_flag & 1
pixel = limit(v_top, pixTop, pixBottom)
dy = (pixel - s.mouseLoc.v)
if (buttondown && dy)
s.mouseLoc.v = pixel
newdivider = limit(divider - dy/plotHeight, 0.1, 0.9)
if (newdivider == divider)
continue
endif
divider = newdivider
ModifyGraph axisEnab(ax1)={divider + 0.025, 1}
ModifyGraph axisEnab(ax2)={0, divider - 0.025}
DrawAction /W=$s.winName getgroup=theline, delete
SetDrawEnv linefgc=(0x2222,0x2222,0x2222), dash=1
SetDrawEnv gstart, gname=theline
DrawLine -0.2, 1-divider, 1.2, 1-divider
SetDrawEnv /W=$s.winName gstop, gname=theline
DoUpdate /W=$s.winName
endif
while (buttondown)
endif
end
January 8, 2022 at 05:49 am - Permalink
Very nice. I had to add
pixDivider = pixDivider *96/72
after the pixDivider declaration to get the cursor to align with the split between the two plots on my Win10 machine (IP9).
January 8, 2022 at 09:33 am - Permalink
In reply to Very nice. I had to add … by jtigor
Rats- bitten yet again by windows coordinates!
I changed the line
to
That should fix it.
January 8, 2022 at 10:10 am - Permalink
Excellent snippet Tony. Works very nice :) I wonder if such code could be added to the Split Axes package in the future. I tried myself at optimizing the code a bit. Here is a CPU-friendly alternative without the do-while loop (the divider positions work fine here on windows; I hope there is no strange offset on Mac):
if (!(s.eventCode==3 || s.eventCode==4 || s.eventCode==5))
return 0
endif
int status = str2num(GetUserData(s.winName, "", "")), doDraw = 0
variable divider
string strInfo = StringByKey("axisEnab(x)", AxisInfo(s.winName, "ax1"), "=")
sscanf strInfo, "{%f,", divider
divider -= 0.025
GetWindow $s.winName, psizeDC
int plotHeight = v_bottom - v_top
variable pixDivider = v_top + (1-divider)*plotHeight
variable pixTop = v_top + 0.1 * plotHeight
variable pixBottom = v_top + 0.9 * plotHeight
int mouseInRange = abs(s.mouseloc.v-pixDivider) < 5
Switch (s.eventCode)
case 3: // mousedown
if (mouseInRange)
SetWindow $s.winName userdata="1"
endif
break
case 4: // mousemoved
DrawAction /W=$s.winName getgroup=theline, delete
if (status == 1)
divider = limit(1-s.mouseloc.v/plotHeight, 0.05, 0.85) + 0.05 // set new divider
doDraw = 1
ModifyGraph axisEnab(ax1)={divider + 0.025, 1}
ModifyGraph axisEnab(ax2)={0, divider - 0.025}
elseif (mouseInRange)
doDraw = 1
s.cursorCode = 6
s.doSetCursor = 1
else
s.cursorCode = 0
s.doSetCursor = 1
endif
if (doDraw)
SetDrawEnv linefgc=(0x2222,0x2222,0x2222), dash=1
SetDrawEnv gstart, gname=theline
DrawLine -0.2, 1-divider, 1.2, 1-divider
SetDrawEnv /W=$s.winName gstop, gname=theline
endif
break
case 5: // mouseup
SetWindow $s.winName userdata="0"
if (!mouseInRange)
DrawAction /W=$s.winName getgroup=theline, delete
s.cursorCode = 0
s.doSetCursor = 1
endif
break
EndSwitch
DoUpdate /W=$s.winName
return 1
End
January 9, 2022 at 03:29 am - Permalink
@chozo,
Thanks, that is a more sensible structure. Unfortunately, the mouseup event is sometimes 'missed' using this version, leading to a 'sticky' behaviour.
Also, setting the cursor on all mousemoved events swallows up the UI cursor changes for a graph window, and the divider calculation is a bit off. You caught my missing return statement, but the hook should return 0 to avoid stealing these events. Try this:
if (!(s.eventCode==3 || s.eventCode==4 || s.eventCode==5))
return 0
endif
int doModify = str2num(GetUserData(s.winName, "", ""))
variable divider
string strInfo = StringByKey("axisEnab(x)", AxisInfo(s.winName, "ax1"), "=")
sscanf strInfo, "{%f,", divider
divider -= 0.025
GetWindow $s.winName, psizeDC
int plotHeight = v_bottom - v_top
variable pixDivider = v_top + (1-divider)*plotHeight
variable pixTop = v_top
variable pixBottom = v_bottom
int mouseInRange = abs(s.mouseloc.v-pixDivider) < 5 && s.mouseloc.h < 100
switch (s.eventCode)
case 3: // mousedown
if (mouseInRange)
SetWindow $s.winName userdata="1"
endif
break
case 4: // mousemoved
if (doModify)
GetMouse /W=$s.winName
if (!(V_flag & 1)) // maybe we missed a mouseup event?
SetWindow $s.winName userdata="0"
doModify = 0
endif
endif
DrawAction /W=$s.winName getgroup=theline, delete
if (doModify)
divider = limit((pixBottom - s.mouseloc.v)/plotHeight, 0.1, 0.9) // set new divider
ModifyGraph axisEnab(ax1)={divider + 0.025, 1}
ModifyGraph axisEnab(ax2)={0, divider - 0.025}
endif
if (mouseInRange || doModify)
SetDrawEnv linefgc=(0x2222,0x2222,0x2222), dash=1
SetDrawEnv gstart, gname=theline
DrawLine -0.2, 1-divider, 1.2, 1-divider
SetDrawEnv /W=$s.winName gstop, gname=theline
s.cursorCode = 6
s.doSetCursor = 1
endif
DoUpdate /W=$s.winName
break
case 5: // mouseup
SetWindow $s.winName userdata="0"
break
endswitch
return 0
end
I restricted the 'hot' zone to the left side of the graph window.
Edit: I added a check for missed mouseup event.
January 11, 2022 at 12:32 am - Permalink
Hi Tony, yes works great. I was also thinking of reducing the active area to the edge (maybe also the divider line should be drawn only in this region). Indeed as you write, return 1 was a bit excessive. I added this in because I had problems with accidentally dragging a marquee at the beginning and did not think much of it afterwards. I also don't know why the divider calculation now works fine without the 0.05 units offset. Must have been a glitch when I was testing things. I didn't get problems with a hanging mousedown state, but it is great to have this fallback in place.
Now if some code to detect multiple axes in both horizontal and vertical arrangements would be added, this could actually be a small complete project to dynamically change axis splits. Maybe I try this as a fun weekend project some time.
January 11, 2022 at 01:39 am - Permalink