Link to home
Start Free TrialLog in
Avatar of riceman0
riceman0

asked on

Math to calculate user friendly XY graph axis ticks?


Hello, this is kind of an oddball question.  I'm rolling my own plot widget in .NET (I know, I know, but the UI needs for this particular plot are very application-specific) and I can plot the data just fine (this was actually surprisingly easy) but the only part I'm having trouble with (also surprising) is the calculation of the ticks to plot on the axis.  All plot widgets calculate user friendly tick sizes based on two inputs

* data range (e.g., 0-10 or 0.0001 to 0.003 or 1e23 to 1e25)
* width of the plot (because of the purpose of the ticks are visual aids, you want a comfortable number of ticks in a given span of pixels)

And it will come up with good tick values (5,10,15; or 0.2, 0.4, 0.6, etc) at given locations

I'm having brain block and can't come up with the right equations to come up with user friendly tick values.  And this is just esoteric enough so I can't seem to find good equations on google.  This can't be too hard since every widget does it.  

Does anyone have good equations, or have come across good equations in their travels?

Thanks for any help.

Avatar of mwochnick
mwochnick
Flag of United States of America image

So are you ranges linear, exponential. logarithmic?  its hard to tell from your examples and the computation would be different based on the type of scale you want to represent

Avatar of riceman0
riceman0

ASKER

Linear is perfectly fine.
Avatar of d-glitch
This is certainly a worthy problem.  You probably have to do it for both axes.

Concentrate on the x-axis.

Calculate your required range:  Xmax-Xmin
and look at the mantissa in scientific notation.  

      If it's 1.00 to 1.59 you might want 10 to 17 divisions   spacing 0.1
      If it's 1.60 to 2.99 you might want   8 to 17 divisions   spacing 0.2
      If it's 3.00 to 5.99 you might want   6 to 14 divisions   spacing 0.5
      If it's 6.00 to 9.99 you might want   6 to 12 divisions   spacing 1.0

To find the plotting limits, you would have round you Xmax up and Xmin down to the next tic.

Exactly.   Although I think same algorithm will end up working for both X and Y.  I hit several snags between that formulation and practice.  I would feel bad asking someone to work it out -- I figured there *must* be some math published out there.






... or published code I could crib from.
I've been looking at MatLab and gnuplot.  They both certainly do it.  But I'm not finding code or even search terms.

   http://www.mathworks.com/help/techdoc/ref/axis.html
ASKER CERTIFIED SOLUTION
Avatar of d-glitch
d-glitch
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
I would choose a minimum distance between ticks (in pixels) then maximize the number of ticks rounding to the nearest 1, 2, or 5 (with leading or trailing 0s as needed).
So if you require at least 30 pixels for a tick, then if the graph is 200 pixels, you have room for at most floor(200/30) = 6 ticks (not counting the bottom as a tick since it helps the math). Then take (max - min)/ticks and round it up to the nearest 1, 2, or 5.
For example min = .97, max = 1.05 (max - min)/6 = .013333... rounds to .02 so we use .96, .98, etc.
By the way I thought the astrostatistics link referenced source code, so closed the question.  It doesn't, but I did end up working out some source code that seems to work, attached.  Comments welcome.
Private Class cFriendlyAxisTickCalculator

        Public m_Input_MinLogical As Single
        Public m_Input_MaxLogical As Single
        Public m_Input_PixelSpan As Single
        Public m_Input_IdealPixelDistance As Single = 100

        Public m_Output_ResultsAreValid As Boolean
        Public m_Output_NumberOfTicks As Integer
        Public m_Output_FirstTickLogicalValue As Single
        Public m_Output_TickLogicalInterval As Single
        Public m_Output_FirstTickPixelPosition As Single
        Public m_Output_TickPixelInterval As Single
        Public m_Output_DecimalPlacesToShow As Integer

        Public Sub Calculate()

            m_Output_ResultsAreValid = False

            If m_Input_PixelSpan = 0 Then Return
            If m_Input_MaxLogical <= m_Input_MinLogical Then Return

            Dim deltalogical As Single = m_Input_MaxLogical - m_Input_MinLogical
            Dim log10_deltalogical As Single = Math.Log10(deltalogical)
            Dim fixed_log10_deltalogical As Single = Int(log10_deltalogical)
            Dim logical_per_pixel As Single = deltalogical / m_Input_PixelSpan
            Dim logical_for_ideal_pixel_distance As Single = logical_per_pixel * m_Input_IdealPixelDistance
            Dim LFIPD_normalized_to_fixed_log10 As Single = logical_for_ideal_pixel_distance / 10 ^ (fixed_log10_deltalogical)

            ' fit to 0.1, 0.2, 1.0 (i.e., 1/10, 1/5, or whole integers)

            Dim LFPID_norm_friendly As Single

            If LFIPD_normalized_to_fixed_log10 < 0.15 Then
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical + 1
                LFPID_norm_friendly = 0.1
            ElseIf LFIPD_normalized_to_fixed_log10 < 0.6 Then
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical + 1
                LFPID_norm_friendly = 0.2
            Else
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical
                LFPID_norm_friendly = 1.0
            End If

            If m_Output_DecimalPlacesToShow < 0 Then m_Output_DecimalPlacesToShow = 0

            m_Output_TickLogicalInterval = LFPID_norm_friendly * 10 ^ (fixed_log10_deltalogical)
            m_Output_TickPixelInterval = m_Output_TickLogicalInterval / logical_per_pixel

            If m_Output_TickPixelInterval = 0 Then Return

            Dim z As Single = Fix(m_Input_MinLogical / m_Output_TickLogicalInterval)

            m_Output_FirstTickLogicalValue = (z + 1) * m_Output_TickLogicalInterval
            m_Output_FirstTickPixelPosition = (m_Output_FirstTickLogicalValue - m_Input_MinLogical) / logical_per_pixel
            m_Output_NumberOfTicks = 1 + Int((m_Input_PixelSpan - m_Output_FirstTickPixelPosition) / m_Output_TickPixelInterval)

            m_Output_ResultsAreValid = True

        End Sub

    End Class

Open in new window


Small fix for when low end is negative:
Private Class cFriendlyAxisTickCalculator

        Public m_Input_MinLogical As Single
        Public m_Input_MaxLogical As Single
        Public m_Input_PixelSpan As Single
        Public m_Input_IdealPixelDistance As Single = 80

        Public m_Output_ResultsAreValid As Boolean
        Public m_Output_NumberOfTicks As Integer
        Public m_Output_FirstTickLogicalValue As Single
        Public m_Output_TickLogicalInterval As Single
        Public m_Output_FirstTickPixelPosition As Single
        Public m_Output_TickPixelInterval As Single
        Public m_Output_DecimalPlacesToShow As Integer

        Public m_Temp As Single

        Public Sub Calculate()

            m_Output_ResultsAreValid = False

            If m_Input_PixelSpan = 0 Then Return
            If m_Input_MaxLogical <= m_Input_MinLogical Then Return

            Dim deltalogical As Single = m_Input_MaxLogical - m_Input_MinLogical
            Dim log10_deltalogical As Single = Math.Log10(deltalogical)
            Dim fixed_log10_deltalogical As Single = Int(log10_deltalogical)
            Dim logical_per_pixel As Single = deltalogical / m_Input_PixelSpan
            Dim logical_for_ideal_pixel_distance As Single = logical_per_pixel * m_Input_IdealPixelDistance
            Dim LFIPD_normalized_to_fixed_log10 As Single = logical_for_ideal_pixel_distance / 10 ^ (fixed_log10_deltalogical)

            ' fit to 0.1, 0.2, 1.0 (i.e., 1/10, 1/5, or whole integers)

            m_Temp = LFIPD_normalized_to_fixed_log10

            Dim LFPID_norm_friendly As Single

            If LFIPD_normalized_to_fixed_log10 < 0.15 Then
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical + 1
                LFPID_norm_friendly = 0.1
            ElseIf LFIPD_normalized_to_fixed_log10 < 0.35 Then
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical + 1
                LFPID_norm_friendly = 0.2
            ElseIf LFIPD_normalized_to_fixed_log10 < 0.6 Then
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical + 1
                LFPID_norm_friendly = 0.5
            Else
                m_Output_DecimalPlacesToShow = -fixed_log10_deltalogical
                LFPID_norm_friendly = 1.0
            End If

            If m_Output_DecimalPlacesToShow < 0 Then m_Output_DecimalPlacesToShow = 0

            m_Output_TickLogicalInterval = LFPID_norm_friendly * 10 ^ (fixed_log10_deltalogical)
            m_Output_TickPixelInterval = m_Output_TickLogicalInterval / logical_per_pixel

            If m_Output_TickPixelInterval = 0 Then Return

            Dim z As Single = Fix(m_Input_MinLogical / m_Output_TickLogicalInterval)

            m_Output_FirstTickLogicalValue = z * m_Output_TickLogicalInterval
            If m_Output_FirstTickLogicalValue < m_Input_MinLogical Then m_Output_FirstTickLogicalValue += m_Output_TickLogicalInterval
            m_Output_FirstTickPixelPosition = (m_Output_FirstTickLogicalValue - m_Input_MinLogical) / logical_per_pixel
            m_Output_NumberOfTicks = 1 + Int((m_Input_PixelSpan - m_Output_FirstTickPixelPosition) / m_Output_TickPixelInterval)

            m_Output_ResultsAreValid = True

        End Sub

    End Class

Open in new window