Excel XY Chart (Scatter plot) Data Label No Overlap

后端 未结 2 636
被撕碎了的回忆
被撕碎了的回忆 2021-02-09 23:14

So I\'ve been working on this for the past week. Although it can\'t do miracles, I can say I\'ve got a pretty good result: \

2条回答
  •  忘掉有多难
    2021-02-09 23:57

    Const PIXEL_TO_POINT_RATIO As Double = 0.72 '1 Pixel = 72/96*1 Point
    Const tStep As Double = 0.1
    Const rStep As Double = 0.1
    Dim pCount As Integer
    
    Sub ExampleMain()
    
            RearrangeScatterLabels Sheet5 
    
            RearrangeScatterLabels Sheet25
    
    End Sub
    
    Sub RearrangeScatterLabels(sht As Worksheet)
        Dim plot As Chart
        Dim sCollection As SeriesCollection
        Dim dLabels() As DataLabel
        Dim dPoints() As Point
        Dim xArr(), yArr(), stDevX, stDevY As Double
        Dim x0, x1, y0, y1 As Double
        Dim temp() As Double
        Dim theta As Double
        Dim r As Double
        Dim isOverlapped As Boolean
        Dim safetyNet, validEntry, currentPoint As Integer
    
        Set plot = sht.ChartObjects(1).Chart 'XY chart (scatter plot)
        Set sCollection = plot.SeriesCollection 'All points and labels
        safetyNet = 1
        pCount = (sCollection.Count - 1)
    
        ReDim dLabels(1 To 1)
        ReDim dPoints(1 To 1)
        ReDim xArr(1 To 1)
        ReDim yArr(1 To 1)
    
        For pt = 1 To sCollection(1).Points.Count
            For i = 1 To pCount
                If sCollection(i).Points.Count <> 0 Then
                    'Dynamically expand the arrays
                    validEntry = validEntry + 1
                    If validEntry <> 1 Then
                        ReDim Preserve dLabels(1 To UBound(dLabels) + 1)
                        ReDim Preserve dPoints(1 To UBound(dPoints) + 1)
                        ReDim Preserve xArr(1 To UBound(xArr) + 1)
                        ReDim Preserve yArr(1 To UBound(yArr) + 1)
                    End If
    
                    Set dLabels(i) = sCollection(i).Points(pt).DataLabel 'Store all label objects
                    Set dPoints(i) = sCollection(i).Points(pt)           'Store all point objects
                    temp = getElementDimensions(, dPoints(i))
                    xArr(i) = temp(0) 'Store all points x values
                    yArr(i) = temp(2) 'Store all points y values
                End If
            Next
        Next
    
        If UBound(dLabels) < 2 Then Exit Sub
    
        pCount = UBound(dLabels)
        stDevX = Application.WorksheetFunction.StDev(xArr) 'Get standard deviation for x
        stDevY = Application.WorksheetFunction.StDev(yArr) 'Get standard deviation for y
        If stDevX = 0 Then stDevX = 1
        If stDevY = 0 Then stDevY = 1
        r = 0
    
        For currentPoint = 1 To pCount
            theta = Rnd * 2 * Application.WorksheetFunction.Pi()
            x0 = xArr(currentPoint)
            y0 = yArr(currentPoint)
            x1 = xArr(currentPoint)
            y1 = yArr(currentPoint)
            isOverlapped = True
    
            Do Until Not isOverlapped
                safetyNet = safetyNet + 1
    
                If safetyNet < 500 Then
                    If Not checkForOverlap(dLabels(currentPoint), dLabels, dPoints, plot) Then
                        'No label is within bounds and not overlapping
                        isOverlapped = False
                        r = 0
                        theta = Rnd * 2 * Application.WorksheetFunction.Pi()
                        safetyNet = 1
                    Else
                        'Move label so it does not overlap
                        theta = theta + tStep
                        r = r + rStep * tStep / (2 * Application.WorksheetFunction.Pi())
                        x1 = x0 + stDevX * r * Cos(theta)
                        y1 = y0 + stDevY * r * Sin(theta)
                        dLabels(currentPoint).Left = x1
                        dLabels(currentPoint).Top = y1
                    End If
                Else
                    safetyNet = 1
                    Exit Do
                End If
            Loop
        Next
    End Sub
    
    Function checkForOverlap(ByRef dLabel As DataLabel, ByRef dLabels() As DataLabel, ByRef dPoints() As Point, ByRef dChart As Chart) As Boolean
        checkForOverlap = False 'Return false by default
    
        'Detect label going over chart area
        If detectOverlap(dLabel, , , dChart) Then
            checkForOverlap = True
            Exit Function
        End If
    
        'Detect labels overlap
        For i = 1 To pCount
            If Not dLabel.Left = dLabels(i).Left Then
                If detectOverlap(dLabel, dLabels(i)) Then
                    checkForOverlap = True
                    Exit Function
                End If
            End If
        Next
    
        'Detect label overlap with point
        For i = 1 To pCount
            If detectOverlap(dLabel, , dPoints(i)) Then
                checkForOverlap = True
                Exit Function
            End If
        Next
    End Function
    
    Function getElementDimensions(Optional dLabel As DataLabel, Optional dPoint As Point, Optional dChart As Chart) As Double()
        'Get element dimensions and compensate slack
        Dim eDimensions(3) As Double
    
        'Working in IV quadrant
        If dPoint Is Nothing And dChart Is Nothing Then
            'Get label dimensions and compensate padding
            eDimensions(0) = dLabel.Left + PIXEL_TO_POINT_RATIO * 3                'Left
            eDimensions(1) = dLabel.Left + dLabel.Width - PIXEL_TO_POINT_RATIO * 3 'Right
            eDimensions(2) = dLabel.Top + PIXEL_TO_POINT_RATIO * 6                 'Top
            eDimensions(3) = dLabel.Top + dLabel.Height - PIXEL_TO_POINT_RATIO * 3 'Bottom
        End If
        If dLabel Is Nothing And dChart Is Nothing Then
            'Get point dimensions
            eDimensions(0) = dPoint.Left - PIXEL_TO_POINT_RATIO * 5 'Left
            eDimensions(1) = dPoint.Left + PIXEL_TO_POINT_RATIO * 5 'Right
            eDimensions(2) = dPoint.Top - PIXEL_TO_POINT_RATIO * 5  'Top
            eDimensions(3) = dPoint.Top + PIXEL_TO_POINT_RATIO * 5  'Bottom
        End If
        If dPoint Is Nothing And dLabel Is Nothing Then
            'Get chart dimensions
            eDimensions(0) = dChart.PlotArea.Left + PIXEL_TO_POINT_RATIO * 22                         'Left
            eDimensions(1) = dChart.PlotArea.Left + dChart.PlotArea.Width - PIXEL_TO_POINT_RATIO * 22 'Right
            eDimensions(2) = dChart.PlotArea.Top - PIXEL_TO_POINT_RATIO * 4                           'Top
            eDimensions(3) = dChart.PlotArea.Top + dChart.PlotArea.Height - PIXEL_TO_POINT_RATIO * 4  'Bottom
        End If
    
        getElementDimensions = eDimensions 'Return dimensions array in Points
    End Function
    
    Function detectOverlap(ByVal dLabel1 As DataLabel, Optional ByVal dLabel2 As DataLabel, Optional ByVal dPoint As Point, Optional ByVal dChart As Chart) As Boolean
        'Left, Right, Top, Bottom
        Dim AxL, AxR, AyT, AyB As Double 'First label coordinates
        Dim BxL, BxR, ByT, ByB As Double 'Second label coordinates
        Dim eDimensions() As Double 'Element dimensions
    
        eDimensions = getElementDimensions(dLabel1)
        AxL = eDimensions(0)
        AxR = eDimensions(1)
        AyT = eDimensions(2)
        AyB = eDimensions(3)
    
        If dPoint Is Nothing And dChart Is Nothing Then
            'Compare with another label
            eDimensions = getElementDimensions(dLabel2)
        End If
        If dLabel2 Is Nothing And dChart Is Nothing Then
            'Compare with a point
            eDimensions = getElementDimensions(, dPoint)
        End If
        If dPoint Is Nothing And dLabel2 Is Nothing Then
            'Compare with chart area
            eDimensions = getElementDimensions(, , dChart)
        End If
        BxL = eDimensions(0)
        BxR = eDimensions(1)
        ByT = eDimensions(2)
        ByB = eDimensions(3)
    
        If dChart Is Nothing Then
            detectOverlap = (AxL <= BxR And AxR >= BxL And AyT <= ByB And AyB >= ByT) 'Reverse De Morgan's Law
        Else
            detectOverlap = Not (AxL >= BxL And AxR <= BxR And AyT >= ByT And AyB <= ByB) 'Is in chart bounds (working in IV quadrant)
        End If
    End Function
    


    I realize the code is kinda rough and not optimized, but I can't spend more time on this project. I've left quite a few notes around to help read it, should anyone choose to continue this project.

    Hope this helps.
    Best wishes, Schadenfreude.

提交回复
热议问题