[[20201020210356]] 『散布図のラベルデータの重なり回避について』(AKI) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『散布図のラベルデータの重なり回避について』(AKI)

シートに以下のようなデータが入っています。

  X  Y
A 4  7
B 6 12
C10 16
D10 16

最大で40行までのデータで、X値とY値の変域はともに最小値4、最大値16です。
このデータを散布図にします。
すると、CとDのように同じX値Y値をとるデータはプロットが重なります。
それはいいのですが、データラベルをセル値にし、「A」や「B」という表示にすると、「C」と「D」という表記も当然重なって表示されます。このラベルの重なりを自動で回避する方法はありますでしょうか。
ドラッグにて移動させればよいのですが、データによっては重複値が多いものもあり、一つひとつ手作業で行うには手間がかかります。

もしくは、シート上で重複値を検索し、重複する値にはそれぞれ順番に0.1ずつ値を加算してずらし、プロットそのものをずらす……なんてことは可能でしょうか。

  X   Y           X    Y
A 4  7      A    4     7
B 6 12   →  B    6    12
C10 16      C 10.1 16.1
D10 16      D 10.2 16.2

どなたかよいお知恵がございましたら、お貸しください。
      
      

< 使用 Excel:Excel2016、使用 OS:Windows10 >


 こんにちは。

 【試し方】
 データラベル付きの散布図を作成します。
 ・データラベルには「セルの値」を表示し、
 ・「引き込み線を引く」にチェックを入れておいて下さい。

 「グラフをアクティブにした状態」で、マクロmainを実行して下さい。

 結果が思わしくなければ、再度mainを実行してみて下さい。
 別の選択肢がでます。

 まずはバックアップを取ったうえで、試して見て下さい。
 少々長いですが、ひとつの標準モジュールにコピーして使います。

 【参考コード】

 Option Explicit

 Dim n_labels     As Long
 Dim myRect()     As Variant
 Dim myRect2()    As Variant
 Dim rectidx1()   As Long
 Dim rectidx2()   As Long
 Dim insideWidth  As Double
 Dim insideHeight As Double

 'アクティブな散布図のデータラベルの重なりをできるだけ回避
 Sub main()
     Dim myChart As Chart

     Set myChart = ActiveChart

     Call pointLabel(myChart)

 End Sub

 Function pointLabel(myChart As Chart)
     Dim bestDisplacement As Variant

     Call getMetrics(myChart)

     'ラベルの位置情報などの取得
     Call getRect(myChart)

     '重なりあう可能性のあるラベルのペアを取得
     Call canIntersect

     '焼きなまし法による最適な変位の探索
     bestDisplacement = simulatedAnnealing()

     'ラベルの位置を実際に動かす
     Call setLabelPosition(myChart, bestDisplacement)

 End Function

 Function getMetrics(myChart As Chart)
     insideWidth = myChart.PlotArea.insideWidth
     insideHeight = myChart.PlotArea.insideHeight
 End Function

 'ラベルの位置情報などを取得
 Sub getRect(myChart As Chart)
     Dim mySeries    As Series
     Dim dl          As DataLabel
     Dim k&
     Dim l#, t#, r#, b#, width#, height#

     Set mySeries = myChart.SeriesCollection(1)
     myChart.SetElement msoElementDataLabelCenter
     '''myChart.SetElement msoElementDataLabelRight

     With mySeries.DataLabels
         .ShowRange = True           '「セルの値」を表示
         .ShowValue = False          'Y値を非表示 するものとした
     End With

     n_labels = mySeries.Points.Count

     ReDim myRect(1 To n_labels)
     ReDim myRect2(1 To n_labels)

     For k = 1 To n_labels
         Set dl = mySeries.Points(k).DataLabel
         '位置は相対化するものとした。最後に元に戻す
         l = dl.Left / insideWidth
         t = dl.Top / insideHeight
         width = dl.width / insideWidth
         height = dl.height / insideHeight

         myRect(k) = Array(l, t, l + width, t + height)

         '仮にラベルの大きさが2倍だったとしたときのラベルの位置(動かす最大限)
         myRect2(k) = Array(l - 0.5 * width, t - 0.5 * height, _
                            l + width * 1.5, t + 1.5 * height)
     Next

 End Sub

 ' 重なりあう可能性のあるラベルのペアを取得(ラベルの大きさが2倍と仮定)
 'これ以外のペアはラベル位置を動かしても重なることはない。(計算負荷の軽減)
 Function canIntersect()
     Dim i&, j&, k&

     k = 0
     For i = 1 To n_labels
         For j = 1 To i - 1
             If rect_intersect(myRect2(i), myRect2(j)) > 0 Then
                 k = k + 1
                 ReDim Preserve rectidx1(1 To k)
                 ReDim Preserve rectidx2(1 To k)
                 rectidx1(k) = i
                 rectidx2(k) = j
             End If
         Next
     Next
 End Function

 ' 焼きなまし法による探索(返り値は、最適な変位組み合わせ)
 Function simulatedAnnealing() As Variant
     ReDim displacement(1 To n_labels) As Variant
     Dim newdisplacement     As Variant
     Dim bestDisplacement    As Variant
     Dim score#
     Dim newscore#
     Dim bestscore#
     Dim temperature#
     Dim i&, j&, k&
     Dim l#, t#

     'ラベルの変位codeを8に初期設定
     For k = 1 To n_labels
         displacement(k) = 8
     Next

     score = makeScores(displacement) '「重なり度合い」の計算
     bestDisplacement = displacement
     bestscore = score
     temperature = 0.001       ' パラメータ

     For i = 1 To 50
         k = 1
         For j = 1 To 50
             newdisplacement = displacement
             'ランダムなラベルをランダムに変位させる
             newdisplacement(WorksheetFunction.RandBetween(1, n_labels)) _
                      = WorksheetFunction.RandBetween(1, 8)
             newscore = makeScores(newdisplacement)

             '高温では緩い条件でも新しい変位に変更   (冒険を許す)
             '(低温になるとより厳しい条件で変更する)(保守的にする)
             If (newscore <= score Or Rnd() < Exp((score - newscore) / temperature)) Then
                 k = k + 1
                 score = newscore
                 displacement = newdisplacement
             End If
             If score <= bestscore Then
                 bestscore = score
                 bestDisplacement = displacement
             End If

             If (bestscore = 0 Or k = 10) Then Exit For
         Next

         If bestscore = 0 Then Exit For
         temperature = 0.9 * temperature '温度を一定率で低下させる
     Next

     simulatedAnnealing = bestDisplacement
 End Function

 'ラベルどうしの「重なり度合い」を評価(ただし、対象は、重なる可能性のあるラベルのペアだけに限定)
 Function makeScores(displacement) As Double
     Dim area#
     Dim k&
     Dim rect1, rect2

     area = 0
     For k = 1 To UBound(rectidx1)
         rect1 = myRect(rectidx1(k))
         rect1 = make_rect(rect1, displacement(rectidx1(k)))

         rect2 = myRect(rectidx2(k))
         rect2 = make_rect(rect2, displacement(rectidx2(k)))
         area = area + rect_intersect(rect1, rect2)
     Next
     makeScores = area
 End Function

 '変位 code(1〜8で指定) に応じて、移動後のラベル位置を返す
 Function make_rect(rect, code) As Variant
     Dim l#, t#, r#, b#, width#, height#
     Dim xdisp#, ydisp#

     l = rect(0): t = rect(1): r = rect(2): b = rect(3)
     width = r - l
     height = t - b
     xdisp = Array(0, -1, -1, -1, 0, 0, 1, 1, 1)(code) * width / 2#
     ydisp = Array(0, -1, 0, 1, -1, 1, -1, 0, 1)(code) * height / 2#

     make_rect = rect_displace(rect, xdisp, ydisp)

 End Function

 'ラベルの位置を指定した分だけずらす
 Function rect_displace(rect, xdisp, ydisp) As Variant
     rect_displace = Array(rect(0) + xdisp, rect(1) + ydisp, _
                           rect(2) + xdisp, rect(3) + ydisp)
 End Function

 'ラベル rect1 と rect2 の重なった部分の面積を返す
 Function rect_intersect(rect1 As Variant, rect2 As Variant) As Double
     Dim l1#, t1#, r1#, b1#
     Dim l2#, t2#, r2#, b2#
     Dim w#, h#

     l1 = rect1(0): t1 = rect1(1): r1 = rect1(2): b1 = rect1(3)
     l2 = rect2(0): t2 = rect2(1): r2 = rect2(2): b2 = rect2(3)

     w = WorksheetFunction.Min(r1, r2) - WorksheetFunction.Max(l1, l2)
     h = WorksheetFunction.Min(b1, b2) - WorksheetFunction.Max(t1, t2)

     If w <= 0 Then
         rect_intersect = 0
         Exit Function
     End If
     If h <= 0 Then
         rect_intersect = 0
         Exit Function
     End If
     rect_intersect = w * h

 End Function

 Function setLabelPosition(myChart As Chart, bestDisplacement As Variant)
     Dim k&
     Dim l#, t#
     ReDim myRectReal(1 To n_labels) As Variant

     'データラベルの位置を実際に移動
     For k = 1 To n_labels
         myRectReal(k) = make_rect(myRect(k), bestDisplacement(k))
         l = myRectReal(k)(0) * insideWidth
         t = myRectReal(k)(1) * insideHeight
         myChart.SeriesCollection(1).Points(k).DataLabel.Left = l
         myChart.SeriesCollection(1).Points(k).DataLabel.Top = t
     Next
 End Function

(γ) 2020/10/21(水) 12:14


Yさん ありがとうございます。
感動ですね。やりたいことがその通りにできました!
大変助かりました!
(AKI) 2020/10/21(水) 19:47

コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.