Modifying enabled axis ranges interactively

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

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.

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?

I think thomas wants to change the range over which the axis is drawn:

ModifyGraph axisEnab(ax2)={0,0.5}

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.

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.

I have long thought there should be more mouse interaction with axis layout, but have many ideas and limited time.

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

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.

menu "GraphMarquee"	//, dynamic	(commented out since it can cause delay with large graphs)
	"-"
	"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

 

To test it, make thomas' plot, then execute

SetWindow graph0 hook(hDivider)=HookAxisDivider
function HookAxisDivider(STRUCT WMWinHookStruct &s)
		
	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

 

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

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):

Function HookAxisDivider(STRUCT WMWinHookStruct &s)
	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

 

@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:

function HookAxisDivider(STRUCT WMWinHookStruct &s)

	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.

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.