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.