Pareto charts in Excel with VBA macros
On this page I show how to make Pareto charts/diagrams in Excel fast and easy using VBA macros. The source values for the chart can be in the same workbook as the macros or another.
You can also download a zip compressed workbook with the macros and start immediately.
A Pareto chart is a chart that contains bars and a line graph, where the bar values are in descending order, and the cumulative total is represented by a line.
It can look like this with fictive values for the excuses people use for being late to work.
Pareto diagrams are used to visualize the most important factors, e.g. errors in a process.
Vilfredo Pareto (1848 - 1923) was an Italian economist, who discovered that 20% of the population owned 80% of the land, but you can read about that elsewhere - this is about VBA.
Personally I have no difficulty spotting the highest columns, if they are not left aligned, but the Pareto chart is a Lean Six Sigma darling, and if that's what people want, we want to make them fast and easy!
The macros on this page will do the following:
- Ask the user to select the input values. The values can be in the same workbook as the macros or another and can be in a column or a row
- Ask the user where to put the sorted table. The table must be sorted to left align the highest bars.
- If the total of some records is less than 5%, the user will have the option of pooling these in a record called "Others".
- The input values will be sorted in a new table, and the chart is inserted.
- Of course the code will check if the input values are valid, and it also checks if the new, sorted table intersects with the input range.
You can copy the code below to a VBA module, where you also make a simple userform with a label for text and two command buttons for "OK" and "Cancel". Or you can just download the workbook with the macros.
The following macros use:
- A modeless ("floating") userform
- Ranges selected by the user
- Dynamic ranges
- Arrays
- Copying from range to array and vice versa
- Application.Intersect to check if ranges overlap
- Loops
If you want to do it from scratch, start by making a userform with a label and 2 command buttons. In my example the userform is called "frmSelect," the OK button "cmdOK" and the Cancel button "cmdCancel". Here is the userform's code:
Private Sub UserForm_Initialize()
If bInput Then
Label1.Caption = "Select the cells (row or column) with " & _
"the input values - and only them." & _
"No total, no headlines - just the raw values. " & _
"It can be in this or another workbook."
Me.Caption = "Select input values"
Else
Label1.Caption = "The macro must insert a table with " & _
lRows & " rows and " & 5 & _
" columns. Select a cell for the table's " & _
"upper left cell. It can be in this or " & _
"another workbook."
Me.Caption = "Select where to insert"
End If
End Sub
Private Sub cmdOK_Click()
Unload Me
If bInput Then
Module1.Analyze
Else
Module1.MakeTable
End If
End Sub
Private Sub cmdCancel_Click()
Unload Me
ThisWorkbook.Activate
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = 0 Then cmdCancel_Click
End Sub
That was our userform. The following code is for Module1. We start by declaring some variables on module level, i.e. at the top.
Option Explicit
Public bAbort As Boolean
Public bInput As Boolean
Dim bVertical As Boolean
Public lRows As Long
Dim sHeader As String
Dim sLabelHead As String
Dim rCell As Range
Dim rInput As Range
Dim rLabels As Range
Dim rTotInput As Range
Dim arTable
And now to the procedure that starts the action.
Sub Start()
On Error GoTo ErrorHandle
bAbort = False
bVertical = False
sHeader = ""
sLabelHead = ""
bInput = True
frmSelect.Show vbModeless
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure Start"
End Sub
When the user has selected the input values and clicked OK, the userform's OK-button calls the following procedure for checking if the values are valid.
Sub Analyze()
On Error GoTo ErrorHandle
Set rInput = Selection
If rInput.Count = 1 Then
MsgBox "You selected only 1 cell. Try again!"
bAbort = True
GoTo BeforeExit
End If
For Each rCell In rInput
If IsNumeric(rCell.Value) = False Then
MsgBox "Cell " & rCell.Address & _
" has a non numeric value."
rCell.Select
bAbort = True
GoTo BeforeExit
End If
Next
With rInput
If .Item(1).Row <> .Item(2).Row Then
bVertical = True
If .Column = 1 Then
MsgBox "There must be categories in " & _
"the column to the left of the values."
GoTo BeforeExit
End If
End If
If bVertical Then
If .Columns.Count > 1 Then
MsgBox "You selected more than one column"
ThisWorkbook.Activate
GoTo BeforeExit
End If
If .Column > 1 Then
Set rLabels = .Offset(0, -1)
End If
If .Item(1).Row > 1 Then
If Len(.Item(1).Offset(-1, 0).Value) > 0 Then
sHeader = .Item(1).Offset(-1, 0).Value
sLabelHead = .Item(1).Offset(-1, -1).Value
Set rTotInput = _
Range(rLabels.Item(1).Offset(-1, 0), _
rInput.Item(rInput.Count))
Else
Set rTotInput = _
Range(rLabels.Item(1), _
rInput.Item(rInput.Count))
End If
End If
Else
If .Rows.Count > 1 Then
MsgBox "You selected more than 1 row"
ThisWorkbook.Activate
GoTo BeforeExit
End If
If .Row > 1 Then
Set rLabels = .Offset(-1, 0)
Else
MsgBox "With input values in a row, " & _
"the categories must be in the row above."
GoTo BeforeExit
End If
If .Item(1).Column > 1 Then
sHeader = .Item(1).Offset(0, -1).Value
sLabelHead = .Item(1).Offset(-1, -1).Value
Set rTotInput = _
Range(rLabels.Item(1).Offset(0, -1), _
rInput.Item(rInput.Count))
Else
Set rTotInput = _
Range(rLabels.Item(1), _
rInput.Item(rInput.Count))
End If
End If
End With
lRows = rInput.Count + 1
bInput = False
frmSelect.Show vbModeless
BeforeExit:
On Error Resume Next
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure Analyze"
bAbort = True
Resume BeforeExit
End Sub
If we got this far, the input values were valid, and it is time to make the chart. frmSelect will ask the user to select a cell as target for the top left cell in the new table. When done, the following procedure is called.
Sub MakeTable()
Dim lCount As Long
Dim lLow As Long
Dim dOthers As Double
Dim dPct As Double
Dim dSum As Double
Dim sAddress As String
Dim rFirstCell As Range
Dim rNewTable As Range
Dim vInput
Dim arTemp()
Dim arPct()
On Error GoTo ErrorHandle
Application.ScreenUpdating = False
Set rFirstCell = Selection
Set rCell = rFirstCell.Resize(rInput.Count + 1, 5)
Set rCell = Application.Intersect(rCell, rTotInput)
If Not rCell Is Nothing Then
MsgBox "The new table will overwrite " & vbNewLine & _
"input data. Select another cell.", , "Conflict"
GoTo BeforeExit
End If
If rFirstCell.Count > 1 Then
Set rFirstCell = rFirstCell.Item(1)
End If
With rFirstCell
.Value = sLabelHead
.Offset(0, 1).Value = sHeader
If bVertical Then
rLabels.Copy .Offset(1, 0)
rInput.Copy .Offset(1, 1)
Else
For lCount = 1 To rInput.Count
.Offset(lCount).Value = rLabels.Item(lCount).Value
.Offset(lCount, 1).Value = rInput.Item(lCount).Value
Next
End If
End With
Set rNewTable = Range(rFirstCell, _
rFirstCell.Offset(lRows - 1, 1))
rNewTable.Sort Key1:=rNewTable.Item(2), _
Order1:=xlDescending, Header:=xlGuess, _
OrderCustom:=1, MatchCase:=False, _
Orientation:=xlTopToBottom, _
DataOption1:=xlSortNormal
dSum = WorksheetFunction.Sum(rInput)
arTemp = rNewTable.Value
ReDim arPct(1 To UBound(arTemp))
For lCount = UBound(arTemp) To 2 Step -1
arPct(lCount) = arTemp(lCount, 2) * 100 / dSum
If arPct(lCount) + dPct < 5 Then
lLow = lLow + 1
dPct = dPct + arPct(lCount)
dOthers = _
dOthers + rFirstCell.Offset(lCount - 1, 1).Value
Else
Exit For
End If
Next
If lLow > 1 Then
vInput = MsgBox("The " & lLow & _
" smallest categories are less than 5% in total." & _
vbNewLine & _
"Do you want to pool them in one category " & _
"called ""Others""?", vbYesNo, "Small categories")
If vInput = vbYes Then
For lCount = lRows To lRows - lLow + 1 Step -1
rNewTable.Rows(lCount).Delete
Next
With rFirstCell
.Offset(lCount).Value = "Others"
.Offset(lCount, 1).Value = dOthers
End With
Set rNewTable = _
rNewTable.Resize(rNewTable.Rows.Count + 1)
lRows = rNewTable.Rows.Count
rNewTable.Sort Key1:=rNewTable.Item(2), _
Order1:=xlDescending, Header:=xlGuess, _
OrderCustom:=1, MatchCase:=False, _
Orientation:=xlTopToBottom, _
DataOption1:=xlSortNormal
End If
End If
ReDim arTemp(1 To lRows, 1 To 3)
Set rFirstCell = rNewTable.Item(2)
Set rNewTable = rNewTable.Resize(lRows, 4)
rNewTable.Select
arTemp(1, 1) = "Akk. %"
arTemp(1, 2) = "'80%"
arTemp(1, 3) = "Acc. frequency"
With rFirstCell
arTemp(2, 1) = _
"=" & .Offset(1, 3).Address & _
"*100/" & .Offset(lRows - 1, 3).Address
arTemp(2, 2) = 80
arTemp(2, 3) = "=" & .Offset(1, 0).Address
For lCount = 3 To lRows
arTemp(lCount, 1) = _
"=" & .Offset(lCount - 1, 3).Address & _
"*100/" & .Offset(lRows - 1, 3).Address
arTemp(lCount, 2) = 80
arTemp(lCount, 3) = _
"=" & .Offset(lCount - 1, 0).Address & _
"+" & .Offset(lCount - 2, 3).Address
Next
End With
Set rFirstCell = rFirstCell.Offset(0, 1)
Set rFirstCell = rFirstCell.Resize(lRows, 3)
rFirstCell.Formula = arTemp
Set rCell = rFirstCell.Resize(lRows - 1, 4)
Set rCell = rCell.Offset(1, -1)
With rCell
.NumberFormat = "0"
.Columns(3).NumberFormat = "#.00"
End With
MakeParetoChart rNewTable
BeforeExit:
On Error Resume Next
Set rCell = Nothing
Set rInput = Nothing
Set rLabels = Nothing
Set rFirstCell = Nothing
Set rNewTable = Nothing
Set rTotInput = Nothing
Erase arPct
Erase arTable
Erase arTemp
Application.ScreenUpdating = True
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure MakeTable"
End Sub
The only thing left is to make the chart.
Sub MakeParetoChart(ByVal rTable As Range)
Dim sSheet As String
On Error GoTo ErrorHandle
sSheet = ActiveSheet.Name
Charts.Add
ActiveChart.ChartType = xlColumnClustered
ActiveChart.SetSourceData _
Source:=Sheets(sSheet).Range(rTable.Address)
ActiveChart.Location Where:=xlLocationAsObject, Name:=sSheet
ActiveChart.SeriesCollection(2).Select
ActiveChart.SeriesCollection(2).ChartType = xlLineMarkers
ActiveChart.SeriesCollection(3).Select
With Selection.Border
.ColorIndex = 5
.Weight = xlMedium
.LineStyle = xlContinuous
End With
Selection.Shadow = False
Selection.InvertIfNegative = False
With Selection.Interior
.ColorIndex = 5
.Pattern = xlSolid
End With
ActiveChart.SeriesCollection(3).AxisGroup = 2
ActiveChart.SeriesCollection(3).ChartType = xlLine
ActiveChart.SeriesCollection(2).Select
With Selection.Border
.ColorIndex = 3
.Weight = xlMedium
.LineStyle = xlContinuous
End With
With Selection
.MarkerBackgroundColorIndex = 3
.MarkerForegroundColorIndex = 3
.MarkerStyle = xlSquare
.Smooth = False
.MarkerSize = 7
.Shadow = False
End With
ActiveChart.SeriesCollection(2).AxisGroup = 2
ActiveChart.PlotArea.Select
With Selection.Border
.ColorIndex = 16
.Weight = xlThin
.LineStyle = xlContinuous
End With
With Selection.Interior
.ColorIndex = 2
.PatternColorIndex = 1
.Pattern = xlSolid
End With
ActiveChart.Axes(xlValue, xlSecondary).Select
With ActiveChart.Axes(xlValue, xlSecondary)
.MinimumScaleIsAuto = True
.MaximumScale = 100
.MinorUnitIsAuto = True
.MajorUnitIsAuto = True
.Crosses = xlAutomatic
.ReversePlotOrder = False
.ScaleType = xlLinear
.DisplayUnit = xlNone
End With
ActiveChart.SeriesCollection(1).Select
With Selection.Border
.ColorIndex = 1
.Weight = xlThin
.LineStyle = xlContinuous
End With
Selection.Shadow = False
Selection.InvertIfNegative = False
With Selection.Interior
.ColorIndex = 15
.Pattern = xlSolid
End With
With ActiveChart.ChartGroups(1)
.Overlap = 0
.GapWidth = 0
.HasSeriesLines = False
.VaryByCategories = False
End With
With ActiveChart
.HasTitle = True
.ChartTitle.Characters.Text = "Title"
End With
Exit Sub
ErrorHandle:
MsgBox Err.Description & " Procedure MakeParetoChart"
End Sub
Quite a lot of code was needed, but now you can make Pareto diagrams quickly, and the chart's values don't have to be in the same workbook as the macros.
If you want the chart to look different, you must either change it manually or modify the code (e.g. by using the macro recorder).
Related:
|