I have design the following code. I would like to understand if a named range can be used in the ws.cells(Y,2)? I have tried to name the code ws.Range("Name") but it failed. The intent is to search a column of data seeking out specific criteria (bold and <1). Once found, it populates the data results to another sheet. The search should be from top to bottom, until it finds the first 7 matches to the criteria. I am seeking assistance with writing the code so that is it 1) cleaner and 2) faster.
X = 12
Y = 4
Z = 0
Set ws = Worksheets("Schedule")
Do Until Z = 7
If ws.Cells(Y, 2).font.Bold = True And ws.Cells(Y, 2) < 1 Then
ws.Activate
ws.Cells(Y, 2).Offset(rowOffset:=0, columnOffset:=1).Activate
ActiveCell.Copy Destination:=Worksheets("Project Status").Cells(X, 3)
ws.Cells(Y, 2).Offset(rowOffset:=0, columnOffset:=3).Activate
ActiveCell.Copy Destination:=Worksheets("Project Status").Cells(X, 6)
ws.Cells(Y, 2).Offset(rowOffset:=0, columnOffset:=4).Activate
ActiveCell.Copy Destination:=Worksheets("Project Status").Cells(X, 7)
ws.Cells(Y, 2).Offset(rowOffset:=0, columnOffset:=0).Activate
ActiveCell.Copy Destination:=Worksheets("Project Status").Cells(X, 8)
X = X + 1
Y = Y + 1
Z = Z + 1
Else
Y = Y + 1
End If
Loop
Name range is a workbook level range, not a worksheet level range.
If the name range refers to the active sheet, then ws.range("name") will work. But if it refers to a non-active sheet, ws.range("name") will throw an error.
Because name range is a workbook level range, so you can simply do Range("name"). Then you'll not get the error above.
P/S: another way to write Range("Name") is [Name] which looks cleaner but missing the intellisense.
The following code does not address "sub question" in respect to *named ranges" as I did not understand that part.
Yet, the following code is a bit shorter and maybe even easier to read. Also, some minor improvements were made in respect to speed:
Option Explicit
Public Sub tmpSO()
Dim WS As Worksheet
Dim X As Long, Y As Long, Z As Long
X = 12
Z = 0
Set WS = ThisWorkbook.Worksheets("Schedule")
With Worksheets("Project Status")
For Y = 4 To WS.Cells(WS.Rows.Count, 2).End(xlUp).Row
If WS.Cells(Y, 2).Font.Bold And WS.Cells(Y, 2).Value2 < 1 Then
WS.Cells(Y, 2).Offset(0, 1).Copy Destination:=.Cells(X, 3)
WS.Cells(Y, 2).Offset(0, 3).Copy Destination:=.Cells(X, 6)
WS.Cells(Y, 2).Offset(0, 4).Copy Destination:=.Cells(X, 7)
WS.Cells(Y, 2).Offset(0, 0).Copy Destination:=.Cells(X, 8)
X = X + 1
Z = Z + 1
' Else
' Y = Y + 1
End If
If Z = 7 Then Exit For
Next Y
End With
End Sub
Maybe you can elaborate a bit more why you want to use named ranges and what you wish to achieve with them that you cannot achieve with the above code as is.
Update:
Miqi180 made me aware that there might be a performance difference when avoiding Offset by directly referencing the cells instead. So, I staged a small performance test on my system (Office 2016, 64-bit) to test this assumption. Apparently, there is a major performance difference of ~14% (comparing the average of 10 iterations using Offset and another 10 iterations avoiding it).
This is the code I used to test the speed difference. Please do let me know if you believe that this setup is flawed:
Option Explicit
' Test whether you are using the 64-bit version of Office.
#If Win64 Then
Declare PtrSafe Function getTickCount Lib "kernel32" Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long
#Else
Declare Function getTickCount Lib "kernel32" Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long
#End If
Public Sub SpeedTestDirect()
Dim i As Long
Dim ws As Worksheet
Dim dttStart As Date
Dim startTime As Currency, endTime As Currency
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Set ws = ThisWorkbook.Worksheets(1)
ws.Cells.Delete
dttStart = Now
getTickCount startTime
For i = 1 To 1000000
ws.Cells(i, 1).Value2 = 1
ws.Cells(i, 2).Value2 = 1
ws.Cells(i, 3).Value2 = 1
ws.Cells(i, 4).Value2 = 1
ws.Cells(i, 5).Value2 = 1
ws.Cells(i, 6).Value2 = 1
Next i
Application.EnableEvents = True
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
getTickCount endTime
Debug.Print "Runtime: " & endTime - startTime, Format(Now - dttStart, "hh:mm:ss")
End Sub
Public Sub SpeedTestUsingOffset()
Dim i As Long
Dim ws As Worksheet
Dim dttStart As Date
Dim startTime As Currency, endTime As Currency
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Set ws = ThisWorkbook.Worksheets(1)
ws.Cells.Delete
dttStart = Now
getTickCount startTime
For i = 1 To 1000000
ws.Cells(i, 1).Offset(0, 0).Value2 = 1
ws.Cells(i, 1).Offset(0, 1).Value2 = 1
ws.Cells(i, 1).Offset(0, 2).Value2 = 1
ws.Cells(i, 1).Offset(0, 3).Value2 = 1
ws.Cells(i, 1).Offset(0, 4).Value2 = 1
ws.Cells(i, 1).Offset(0, 5).Value2 = 1
Next i
Application.EnableEvents = True
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
getTickCount endTime
Debug.Print "Runtime: " & endTime - startTime, Format(Now - dttStart, "hh:mm:ss")
End Sub
Based on this finding the improved code should be (thanks to Miqi180):
Public Sub tmpSO()
Dim WS As Worksheet
Dim X As Long, Y As Long, Z As Long
X = 12
Z = 0
Set WS = ThisWorkbook.Worksheets("Schedule")
With Worksheets("Project Status")
For Y = 4 To WS.Cells(WS.Rows.Count, 2).End(xlUp).Row
If WS.Cells(Y, 2).Font.Bold And WS.Cells(Y, 2).Value2 < 1 Then
WS.Cells(Y, 3).Copy Destination:=.Cells(X, 3)
WS.Cells(Y, 5).Copy Destination:=.Cells(X, 6)
WS.Cells(Y, 6).Copy Destination:=.Cells(X, 7)
WS.Cells(Y, 2).Copy Destination:=.Cells(X, 8)
X = X + 1
Z = Z + 1
' Else
' Y = Y + 1
End If
If Z = 7 Then Exit For
Next Y
End With
End Sub
Yet, it should be noted that the speed can still be very much improved by moving over to (1) copying values only / directly using .Cells(X, 3).Value2 = WS.Cells(Y, 2).Value2 (for example) and (2) furthermore by using arrays instead.
Of course this does not include yet the standard suggestions such as Application.ScreenUpdating = False, Application.Calculation = xlCalculationManual, and Application.EnableEvents = False.
Related
I'm looking for a way to speed up this code as it takes my computer 20-30 minutes to run. It essentially runs through a list of column values in sheet "A" and if It matches a column value in sheet "B" it will pull the entire corresponding row to the sheet "Match".
Sub MatchSheets()
Dim lastRowAF As Integer
Dim lastRowL As Integer
Dim lastRowM As Integer
Dim foundTrue As Boolean
Application.ScreenUpdating = False
lastRowAF = Sheets("FHA").Cells(Sheets("FHA").Rows.Count, "AF").End(xlUp).Row
lastRowL = Sheets("New Construction").Cells(Sheets("New Construction").Rows.Count, "L").End(xlUp).Row
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row
For i = 1 To lastRowAF
foundTrue = False
For j = 1 To lastRowL
If Sheets("FHA").Cells(i, 32).Value = Sheets("New Construction").Cells(j, 12).Value Then
foundTrue = True
Exit For
End If
Next j
If foundTrue Then
Sheets("FHA").Rows(i).Copy Destination:= _
Sheets("Match").Rows(lastRowM + 1)
lastRowM = lastRowM + 1
End If
Next i
Application.ScreenUpdating = True
End Sub
Collections are optimized for looking values. Using a combination of a Collection and Array is usually the best way to match two list. 20K Rows X 54 Columns (140K Values) took this code 10.87 seconds to copy over on a slow PC.
Sub NewMatchSheets()
Dim t As Double: t = Timer
Const NUM_FHA_COLUMNS As Long = 54, AF As Long = 32
Dim list As Object
Dim key As Variant, data() As Variant, results() As Variant
Dim c As Long, r As Long, count As Long
ReDim results(1 To 50000, 1 To 100)
Set list = CreateObject("System.Collections.ArrayList")
With ThisWorkbook.Worksheets("New Construction")
data = .Range("L1", .Cells(.Rows.count, "L").End(xlUp)).Value
For Each key In data
If key <> "" Then
If Not list.Contains(key) Then list.Add key
End If
Next
End With
With ThisWorkbook.Worksheets("FHA")
data = .Range(.Range("A1").Resize(1, NUM_FHA_COLUMNS), .Cells(.Rows.count, AF).End(xlUp)).Value
For r = 1 To UBound(data)
key = data(r, AF)
If list.Contains(key) Then
count = count + 1
For c = 1 To UBound(data, 2)
results(count, c) = data(r, c)
Next
End If
Next
End With
If count = 0 Then Exit Sub
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = True
With ThisWorkbook.Worksheets("Match")
With .Cells(.Rows.count, "A").End(xlUp)
.Offset(1).Resize(count, NUM_FHA_COLUMNS).Value = results
End With
End With
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = False
Debug.Print Round(Timer - t, 2)
End Sub
use variant arrays:
Sub MatchSheets()
Dim lastRowAF As Long
Dim lastRowL As Long
Dim lastRowM As Long
Application.ScreenUpdating = False
lastRowAF = Sheets("FHA").Cells(Sheets("FHA").Rows.Count, "AF").End(xlUp).Row
lastRowL = Sheets("New Construction").Cells(Sheets("New Construction").Rows.Count, "L").End(xlUp).Row
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row
Dim FHAArr As Variant
FHAArr = Sheets("FHA").Range(Sheets("FHA").Cells(1, 1), Sheets("FHA").Cells(lastRowAF, Columns.Count).End(xlToLeft)).Value
Dim NewConArr As Variant
NewConArr = Sheets("New Construction").Range(Sheets("New Construction").Cells(1, 12), Sheets("New Construction").Cells(lastRowL, 12)).Value
Dim outarr As Variant
ReDim outarr(1 To UBound(FHAArr, 1), 1 To UBound(FHAArr, 2))
Dim k As Long
k = 0
Dim l As Long
For i = 1 To lastRowAF
For j = 1 To lastRowL
If FHAArr(i, 32) = NewConArr(j, 1) Then
For l = 1 To UBound(FHAArr, 2)
k = k + 1
outarr(k, l) = FHAArr(i, l)
Next l
Exit For
End If
Next j
Next i
Sheets("Match").Cells(lastRowM + 1, 1).Resize(UBound(outarr, 1), UBound(outarr, 2)).Value = outarr
Application.ScreenUpdating = True
End Sub
FHA Worksheet: 2500 rows by 50 columnsNew Construction Worksheet: 500 rows by 1 column LMatch Worksheet: 450 transfers from FMA Elapsed time: 0.13 seconds
Get rid of all the nested loop and work with arrays.
Your narrative seemed to suggest that there might be multiple matches for any one value but your code only looks for a single match then Exit For. I'll work with the latter of the two scenarios.
Sub MatchSheets()
Dim i As Long, j As Long
Dim vFM As Variant, vNC As Variant
Debug.Print Timer
With Worksheets("New Construction")
vNC = .Range(.Cells(1, "L"), _
.Cells(.Rows.Count, "L").End(xlUp)).Value2
End With
With Worksheets("FHA")
vFM = .Range(.Cells(1, "A"), _
.Cells(.Rows.Count, _
.Cells(1, .Columns.Count).End(xlToLeft).Column).End(xlUp)).Value2
End With
ReDim vM(LBound(vFM, 2) To UBound(vFM, 2), 1 To 1)
For i = LBound(vFM, 1) To UBound(vFM, 1)
If Not IsError(Application.Match(vFM(i, 32), vNC, 0)) Then
For j = LBound(vFM, 2) To UBound(vFM, 2)
vM(j, UBound(vM, 2)) = vFM(i, j)
Next j
ReDim Preserve vM(LBound(vFM, 2) To UBound(vFM, 2), LBound(vM, 2) To UBound(vM, 2) + 1)
End If
Next i
With Worksheets("match")
.Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0).Resize(UBound(vM, 2), UBound(vM, 1)) = _
Application.Transpose(vM)
End With
Application.ScreenUpdating = True
Debug.Print Timer
End Sub
Try changing this line:
Sheets("FHA").Rows(i).Copy Destination:= _
Sheets("Match").Rows(lastRowM + 1)
For the following line:
Sheets("Match").Rows(lastRowM + 1).Value for Sheets("FHA").Rows(i).value
If you really need to shave milliseconds, you could also set: lastRowM to be:
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row + 1
And use:
Sheets("Match").Rows(lastRowM).Value for Sheets("FHA").Rows(i).value
Thus saving you an addition every time you go through that part of the code
I have a very large data set (600,000 rows) of data structured in the following format:
1) There are around 60 products. One is a Total US number, while the others are for Manufacturers and are labled as KMFs. There are also some labeled as PCKGs(but aren't relevant for this question)
2) Each product is located in 60 different markets
3) Each market has 20 different locations
4) I have 12 metrics for which I need to calculate data in the following manner: Total US number - sum(KMFs) for each metric
I have written vba code for this but it is taking too long to run(around 20 minutes) I need to run similar code on at least 20 worksheets. I have tried various methods such as setting screenUpdating etc. to false. Here is my code. I am new to vba coding so I may have missed obvious things. Please let me know anything is unclear. Please help!
Sub beforeRunningCode()
Application.ScreenUpdating = False
Application.DisplayStatusBar = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
ActiveSheet.DisplayPageBreaks = False
End Sub
Sub returnToOriginal()
Application.ScreenUpdating = screenUpdateState
Application.DisplayStatusBar = statusBarState
Application.Calculation = calcState
Application.EnableEvents = eventsState
ActiveSheet.DisplayPageBreaks = displayPageBreaksState
End Sub
Function LastRowFunc(Sheet) As Long
LastRowFunc = ActiveWorkbook.Worksheets(Sheet).Range("A2", Worksheets(Sheet).Range("A2").End(xlDown)).Rows.Count
End Function
Function LastColFunc(Sheet) As Long
With ActiveSheet
LastColFunc = ActiveWorkbook.Sheets(Sheet).Cells(1, .Columns.Count).End(xlToLeft).Column
End With
End Function
Sub AOCalculate()
Call beforeRunningCode 'Optimize Excel
Dim LastRow As Long
Dim LastCol As Long
Dim Period As String
Dim Sheet As String
Dim Arr(1 To 16)
Dim Count As Integer
Sheet = "Energy_LS_Bottler"
Period = "2016 WAVE 1 - 3 W/E 05/07"
LastRow = LastRowFunc(Sheet) 'Calculate last row for which data exists
LastCol = LastColFunc(Sheet) 'Calculate last column for which data exists
For Each Location In ActiveWorkbook.Sheets("Locations").Range("D7:D28").Value
For Each Market In ActiveWorkbook.Sheets("Markets").Range("A5:A92").Value
Count = Count + 1
Arr(1) = Market
Arr(2) = "AO"
Arr(3) = Location
Arr(4) = Period
With ActiveWorkbook.Sheets(Sheet) 'Filtering for KMF
.AutoFilterMode = False
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=17, Criteria1:="=KMF"
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=1, Criteria1:=Market
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=3, Criteria1:=Location
End With
For k = 5 To 16
Arr(k) = Application.WorksheetFunction.Sum(ActiveWorkbook.Sheets(Sheet).Range(Cells(1, k), Cells(LastRow, k)).SpecialCells(xlCellTypeVisible))
Next k
With ActiveWorkbook.Sheets(Sheet) ' filtering for Total US
.AutoFilterMode = False
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=17, Criteria1:="=Total US"
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=1, Criteria1:=Market
.Range(Cells(1, 1), Cells(LastRow, LastCol)).AutoFilter field:=3, Criteria1:=Location
End With
For k = 5 To 16
Arr(k) = -Arr(k) + Application.WorksheetFunction.Sum(ActiveWorkbook.Sheets(Sheet).Range(Cells(1, k), Cells(LastRow, k)).SpecialCells(xlCellTypeVisible))
Next k
For j = 1 To 16
ActiveWorkbook.Sheets(Sheet).Cells(LastRow + Count, j).Value = Arr(j)
Next j
Erase Arr
Next
Next
ActiveWorkbook.Sheets(Sheet).AutoFilterMode = False
Call returnToOriginal
End Sub
[Edit]: Here is a link to a sample data set https://drive.google.com/file/d/0B3MkGa57h6g_WGl2WWlWekd4NU0/view?usp=sharing
I think that this will work (though I haven't had a chance to test it), and should be a lot faster:
Sub AOCalculate()
Call beforeRunningCode 'Optimize Excel
Dim LastRow As Long
Dim LastCol As Long
Dim Period As String
Dim Sheet As String
Dim Arr() '1 To 2000, 1 To 16)
Dim Count As Integer
Sheet = "Energy_LS_Bottler"
Period = "2016 WAVE 1 - 3 W/E 05/07"
LastRow = LastRowFunc(Sheet) 'Calculate last row for which data exists
LastCol = LastColFunc(Sheet) 'Calculate last column for which data exists
'copy all of the relevant cells to local arrays for speed
Dim Locations(), Markets(), data()
Markets = ActiveWorkbook.Sheets("Markets").Range("A5:A92").Value
Locations = ActiveWorkbook.Sheets("Locations").Range("D7:D28").Value
'(pretty sure the following line needs to localize the Cells() to .Cells())
data = ActiveWorkbook.Sheets(Sheet).Range(Cells(1, 1), Cells(LastRow, LastCol)).Value '**'
ReDim Arr(1 To UBound(Markets, 1) * UBound(Locations, 1), 16)
'make an index of pointers into our accumulation array
Dim counts As New Collection
Dim i As Long, l As Long, m As Long
For l = 1 To UBound(Locations, 1)
Location = Locations(l, 1) '**'
For m = 1 To UBound(Markets, 1)
Market = Markets(m, 1) '**'
i = i + 1
counts.Add i, CStr(Location) & "~" & CStr(Market)
'counts.Add NewAccumArray(Location, Market, Period), CStr(Location) & "~" & CStr(Market)
Arr(i, 1) = Market
Arr(i, 2) = "AO"
Arr(i, 3) = Location
Arr(i, 4) = Period
Next
Next
' go through each row and add it to the appropiate count in the array
Dim r As Long
Dim key As String, idx As Long
For r = 1 To UBound(data, 1)
key = CStr(data(r, 3)) & "~" & CStr(data(r, 1))
If data(r, 17) = "KMF" Then
idx = counts(key)
For k = 5 To 16
Arr(idx, k) = Arr(idx, k) - data(r, k)
Next k
Else
If data(r, 17) = "Total US" Then
idx = counts(key)
For k = 5 To 16
Arr(idx, k) = Arr(idx, k) + data(r, k)
Next k
End If
End If
Next r
' output the results
ActiveWorkbook.Sheets(Sheet).Range(Cells(LastRow + 1, 1), Cells(LastRow + Count, 16)).Value = Arr
ActiveWorkbook.Sheets(Sheet).AutoFilterMode = False
Call returnToOriginal
End Sub
Answering the query "What did I mean by this?"
'(pretty sure the following line needs to localize the Cells() to .Cells())
data = ActiveWorkbook.Sheets(Sheet).Range(Cells(1, 1), Cells(LastRow, LastCol)).Value '**'
The use of Cells(..) here is fundamentally unreliable and broken. this is because Cells(..) is really a shortcut for ActiveSheet.Cells(..) and the Active* properties are inherently slow and unreliable because they can change while the code is running. Worse, this code is assuming that ActiveSheet = Energy_LS_Blotter which is far from certain.
The correct way to write this line would be like this:
data = ActiveWorkbook.Sheets(Sheet).Range( _
ActiveWorkbook.Sheets(Sheet).Cells(1, 1), _
ActiveWorkbook.Sheets(Sheet).Cells(LastRow, LastCol) _
).Value
But that is long, ugly and inconvenient. An easier way would be to use either a Sheet variable, or a With:
With ActiveWorkbook.Sheets(Sheet)
data = .Range(.Cells(1, 1), .Cells(LastRow, LastCol)).Value
End With
I have worked with basic pivot tables for a few years, however I am getting stuck on something that I think should be fairly simple to solve (ugh).
I would like to format my pivot table in a specific way.
For example, just say I am using the following data:
Client Name Stage Amount Paid Date Paid
Client A Start $70,000 1/10/2015
Client A Middle $50,000 1/11/2015
Client A End $30,000 1/12/2015
Client B Start $50,000 5/11/2015
Client B Middle $30,000 5/11/2015
Client B End $50,000 5/12/2015
Client C Start $10,000 10/12/2015
Client C Middle $20,000 20/12/2015
Client C End $30,000 30/12/2015
I would like to arrange the pivot table so that it looks like this:
Table Example with Correct Formatting
The only way I can almost get it to work is if it looks like this:
PivotTable Example - Not correct formatting
I really need the formatting to be exactly like picture one.
Thanks for any help you may be able to provide.
Answers in excel steps or in VBA code would be awesome :)
I think you cannot have exactly the output that you want with a pivote table. So i wrote a code which create first a pivot table as close as you want. And then other macro which will create the exact format table as you put in the picture.
1) However you have (it could be easily automated) to replace in your row data:
Start by 1
Middle by 2
End by 3
And your row data tittles should be in Sheet1 and start cell A1
Main sub to call all codes: (all codes have to be in the same module. How hope it can help you.
Sub main()
Call PivotTable
Call FinalTable
Call DeleteRow
Call FormatTable
End Sub
Here is the first code that create the pivot table:
Sub PivotTable()
Dim PTCache As PivotCache
Dim PT As PivotTable
'1.CREATE DATA STORAGE UNIT
Set PTCache = ActiveWorkbook.PivotCaches.Create( _
SourceType:=xlDatabase, _
SourceData:=Range("A1").CurrentRegion)
'2. ADD WORKSHEET
Worksheets.Add
ActiveSheet.Name = "PivotTable1"
'3.CREATE PIVOT TABLE N*1
Set PT = ActiveSheet.PivotTables.Add( _
PivotCache:=PTCache, _
TableDestination:=Range("A3"))
'4. ENUMERATE PREFERENCES FOR PIVOTE TABLE
With PT
.PivotFields("Client Name").Orientation = xlRowField
.PivotFields("Amount Paid").Orientation = xlRowField
.RowAxisLayout xlTabularRow
End With
'MODIFYING DATA FIELD CALCULATION
With PT.PivotFields("Client Name")
.Subtotals(1) = False
End With
With PT.PivotFields("Date Paid")
.Orientation = xlColumnField
.Caption = " Date Paid"
End With
With PT.PivotFields("Stage")
.Orientation = xlDataField
.Caption = " Stage"
.NumberFormat = "[=1]""Start"";[>2]""End"";""Middle"""
End With
With PT.PivotFields("Amount Paid")
.Orientation = xlDataField
.Function = xlSum
.Caption = " Amount Paid"
End With
Range("C4").Select
Selection.Group Start:=True, End:=True, Periods:=Array(False, False, False, _
False, True, False, False)
PT.DisplayErrorString = False
PT.HasAutoFormat = False
PT.PivotSelect "", xlDataAndLabel, True
Selection.Copy
Worksheets.Add
ActiveSheet.Name = "FinalTable"
Range("A1").PasteSpecial xlPasteValuesAndNumberFormats
Rows(1).Delete
Columns("B").Delete
Columns("I").Delete
Columns("H").Delete
End Sub
To format:
Sub FinalTable()
Dim Nextcell As Double
Dim j As Integer
Lastrow = Sheets("FinalTable").Range("A1").SpecialCells(xlCellTypeLastCell).Row
i = 3
Do Until i = Lastrow
NextProcess i, Nextcell, Lastrow, j
For c = 2 To 7
If j = Lastrow Then Exit Do
If IsEmpty(Cells(i, c)) Then
For j = Nextcell - 1 To i Step -1
If Not IsEmpty(Cells(j, c)) And Not IsEmpty(Cells(j - 1, c)) Then
Range(Cells(j, c), Cells(j - 1, c)).Copy Cells(i, c)
Range(Cells(j, c), Cells(j - 1, c)).ClearContents
Exit For
End If
If Not IsEmpty(Cells(j, c)) Then
Cells(j, c).Copy Cells(i, c)
Cells(j, c).ClearContents
Exit For
End If
If Not IsEmpty(Cells(j - 1, c)) Then
Cells(j - 1, c).Copy Cells(i, c)
Cells(j - 1, c).ClearContents
Exit For
End If
Next j
End If
Next c
StepB = Nextcell - i
i = StepB + i
Loop
i = 2
Do
If Application.WorksheetFunction.CountA(Rows(i)) = 0 Then
Rows(i).Delete
i = i - 1
End If
Lastrow = Sheets("FinalTable").Range("A" & Rows.Count).End(xlUp).Row
i = 1 + i
Loop Until i = Lastrow
End Sub
Code to delete the empty rows in your Final Table
Sub DeleteRow()
Dim Lastrow As Long
Dim i As Integer
Lastrow = Sheets("FinalTable").Range("A" & Rows.Count).End(xlUp).Row
i = 2
Do
If Application.WorksheetFunction.CountA(Rows(i)) = 0 Then
Rows(i).Delete
i = i - 1
End If
Lastrow = Sheets("FinalTable").Range("A" & Rows.Count).End(xlUp).Row
i = 1 + i
Loop Until i = Lastrow
End Sub
Code to put border in your final table:
Sub FormatTable()
Dim Nextcell As Double
Dim j As Integer
Lastrow = Sheets("FinalTable").Range("G" & Rows.Count).End(xlUp).Row
For i = 2 To Lastrow
If Not IsEmpty(Cells(i, 1)) Then
If Not IsEmpty(Cells(i + 1, 1)) Then
Range(Cells(i, 1), Cells(i, 7)).BorderAround
ElseIf Not IsEmpty(Cells(i + 2, 1)) Then
NextProcess i, Nextcell, Lastrow, j
Range(Cells(i, 1), Cells(Nextcell - 1, 7)).BorderAround
Else
Range(Cells(i, 1), Cells(Lastrow, 7)).BorderAround
End If
End If
Range(Cells(1, 2), Cells(Lastrow, 3)).BorderAround
Range(Cells(1, 4), Cells(Lastrow, 5)).BorderAround
Range(Cells(1, 6), Cells(Lastrow, 7)).BorderAround
Next i
End Sub
The subroutine to find the next client name:
Sub NextProcess(ByVal i As Integer, ByRef Nextcell As Double, ByVal Lastrow As Long, ByRef j As Integer)
Dim Found As Boolean
'Dim j As Integer
Found = False
j = i + 1
Do Until Found = True Or Lastrow = j
If Not IsEmpty(Range("A" & j).Value) Then
Nextcell = Cells(j, 1).Row
Found = True
End If
j = j + 1
Loop
End Sub
Here is my problem. I managed to create a macro that looks like this:
Sub Macro1()
Range("G17:G36").Select
ActiveSheet.Shapes.AddChart.Select
ActiveChart.SetSourceData Source:=Range("'Sheet1'!$G$17:$G$36")
ActiveChart.ChartType = xlLine
End Sub
I know this was pretty basic to record but my problem is how to change it and make the range dynamic and conditional. For example when I get to the row 17 I have a value in the cell D17 that is greater than lets say 200 and a value in E17 greater than 100. This should trigger the beginning of my range. So if D17>200 AND E17>100 I need to get G17 as the beginning of the range. As for G36 (the end of the range) the logic is very similar but this time I would test for a condition like this: IF F36<64 THEN get G36 as the end of the range.
The should repeat till the end. For example the last row could be at 28000 so I expect a good few of these charts to be created along the way.
Thanks is advance for your help,
Schroedinger.
This is how it looks now and gives me a run-time error explained in my correspondence with EngJon.
Sub GenerateCharts()
Application.ScreenUpdating = False
'Get the last row
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim endOfRange As Long
Dim wholeRange As Range
Dim i As Long
For i = 1 To LastRow
If Cells(i, 4) > 0.000001 And Cells(i, 5) > 0.00000002 Then
'Determine the end of the range
endOfRange = DetermineRange(i)
Set wholeRange = Range(Cells(i, 7), Cells(endOfRange, 7))
NewChart (wholeRange)
i = endOfRange
End If
Next i
Application.ScreenUpdating = True
End Sub
Function DetermineRange(row As Long) As Long
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim j As Long
For j = row To LastRow
If Cells(j, 6) < -0.0000000018 Then
DetermineRange = j
Exit Function
End If
Next j
DetermineRange = j
End Function
Function NewChart(rng As Range)
ActiveSheet.Shapes.AddChart.Select
ActiveChart.SetSourceData Source:=rng
ActiveChart.ChartType = xlLine
End Function
This is a final solution for me. I hope it helps someone. Big tnx to EngJon and Paagua Grant.
Sub GenerateCharts()
Application.ScreenUpdating = False
Dim StartCell As Long
Dim EndCell As Long
Dim ChartRange As Range
Dim DataEnd As Long
Dim i As Integer
Dim j As Integer
Dim HasStart As Boolean
Dim HasEnd As Boolean
'Sets end of data based on the row you are charting
DataEnd = Cells(Rows.Count, 7).End(xlUp).Row
'Begin loop to find start and end ranges, create charts based on those ranges
For i = 1 To DataEnd
If HasStart Then
If Cells(i, 4).Value < 0 Then
EndCell = i
HasEnd = True
End If
Else 'If there isn't a starting cell yet
If Cells(i, 4).Value > 0.000001 And Cells(i, 5).Value > 0.00000002 Then
StartCell = i
HasStart = True
End If
End If
If HasStart And HasEnd Then
Set ChartRange = ActiveSheet.Range(Cells(StartCell, 7), Cells(EndCell, 7))
ActiveSheet.Shapes.AddChart(xlLine, _
Left:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell, 10)).Left, _
Top:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell, 10)).Top, _
Width:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell, 20)).Width, _
Height:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell + 25, 10)).Height _
).Select
ActiveChart.SetSourceData Source:=ChartRange
HasStart = False
HasEnd = False
End If
Next
Application.ScreenUpdating = True
End Sub
You can use your recorded Macro1 as a Function and call it when you need to create a new Chart:
Function NewChart(rng As Range)
ActiveSheet.Shapes.AddChart.Select
ActiveChart.SetSourceData Source:=rng
ActiveChart.ChartType = xlLine
End Function
You will also need the following function:
Function DetermineRange(row As Long) As Long
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim j As Long
For j = row To LastRow
If Cells(j, 6) < 64 Then
DetermineRange = j
Exit Function
End If
Next j
DetermineRange = j
End Function
You will call it in a Sub that iterates over all rows:
Sub GenerateCharts()
Application.ScreenUpdating = False
'Get the last row
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim endOfRange As Long
Dim wholeRange As Range
Dim i As Long
For i = 1 To LastRow
If Cells(i, 4) > 200 And Cells(i, 5) > 100 Then
'Determine the end of the range
endOfRange = DetermineRange(i)
Set wholeRange = Range(Cells(i, 7), Cells(endOfRange, 7))
NewChart wholeRange
i = endOfRange
End If
Next i
Application.ScreenUpdating = True
End Sub
Copy those three in a module and execute the Sub. Please comment if this did what you needed.
Here's a slightly different option that performs all of the tasks in a single function.
Option Explicit
Private Sub CommandButton1_Click()
Application.ScreenUpdating = False
Dim StartCell As Long, EndCell As Long, ChartRange As Range, DataEnd As Long, i As Integer, j As Integer, HasStart As Boolean, HasEnd As Boolean, _
ChartTop As Long, ChartHeight As Long
DataEnd = Cells(Rows.Count, 7).End(xlUp).Row 'Sets end of data based on the row you are charting.
ChartTop = 50
ChartHeight = 100
'Begin loop to find start and end ranges, create charts based on those ranges.
For i = 1 To DataEnd
If HasStart Then
If Cells(i, 6).Value < 64 Then
EndCell = i
HasEnd = True
End If
Else 'If there isn't a starting cell yet.
If Cells(i, 7).Value > 200 And Cells(i, 5).Value > 100 Then
StartCell = i
HasStart = True
End If
End If
If HasStart And HasEnd Then
Set ChartRange = ActiveSheet.Range(Cells(StartCell, 7), Cells(EndCell, 7))
ActiveSheet.Shapes.AddChart(Top:=ChartTop, Height:=ChartHeight).Select
With ActiveChart
.SetSourceData Source:=ChartRange
.ChartType = xlLine
End With
ChartTop = ChartTop + ChartHeight + 15
HasStart = False
HasEnd = False
End If
Next
Application.ScreenUpdating = True
End Sub
This also makes sure that each chart created by the tool does not overlap the previous chart.
For the sake of space and clarity, I am putting my response to your followup questions here.
Assuming standard row heights and column widths, you can set
ChartTop =(StartCell-1)*15
to set the top of the chart to begin at the top of the same row as your data, and within the
ActiveSheet.Shapes.AddChart(Top:=ChartTop, Height:=ChartHeight).Select
you can add
Left:=(X * 48)
where X is one less than the column number that you want the chart to be left-aligned to, e.g. if you want the chart to start at the left edge of Column I, X would be equal to 8. However, as far as I can tell, there is no easy way to adjust these values if your row height/column widths is non-standard, e.g. if you have auto-fit your columns to your data.
I have a macro which looks at a range of cells. Every other cell is either a 1 or a 0 (sign bit). Depending on the sign bit, the next cell (a normal number) is multiplied either by 1 or 0. I keep getting a run time error 1004 Application-defined or object-defined error on the body of the ElseIf of the If statement (indicated below). Not sure what I'm doing wrong. My code is in a "proof-of-concept" stage so it's still pretty hackish.
Dim N As Long
------------------------------------------
Private Sub CommandButton1_Click()
Dim x As Integer
Dim y As Integer
x = 0
y = 1
N = Application.InputBox(Prompt:="Enter value", Type:=1)
If N > Columns.Count Then
N = Columns.Count
Else
For i = 4 To 9999
Cells(1, i).ClearContents
Cells(3, i).ClearContents
Next i
End If
For i = 4 To N + 3
x = x + y
Cells(1, i) = x
Next i
For i = 4 To N + 3
If Cells(2, i) = 1 Then
Cells(2, i).Offset(0, 1).Select = Cells(2, i).Offset(0, 1).Select * -1
ElseIf Cells(2, i) = 0 Then
'This is the line with errors vvvvvvvvvvvvvvvvv
Cells(2, i).Offset(0, 1).Select = Cells(2, i).Offset(0, 1).Select * 1
End If
Next i
End Sub
That's because you're using Select. Obviously, Select and Activate don't give you values. They select or activate the cell, not different from manually clicking on them using the mouse or moving/activating to them using the keyboard or what else. Multiplying them by a value is a major no-no.
The Range property you should be looking for is Value. In any case, I think you're making it difficult because of having two loops. You really should reconsider your design pattern. In any case, here's my approach (mine's vertical, but it seems like yours is horizontal, so be clear exactly what is on your end so this can be adjusted).
Private Sub CommandButton1_Click()
Dim WS As Worksheet
Dim LastRow As Long
Dim Iter As Long
Dim CurrCell As Range
Const Col = 1
With ThisWorkbook
Set WS = .Sheets("Sheet3") 'Change as necessary.
End With
With WS
LastRow = .Range("A" & .Rows.Count).End(xlUp).Row
For Iter = 1 To LastRow 'Change as necessary.
Set CurrCell = .Cells(Iter, Col)
Select Case CurrCell.Value
Case 1
CurrCell.Offset(0, 1).Value = (CurrCell.Offset(0, 1).Value * (-1))
Case 0
CurrCell.Offset(0, 1).Value = (CurrCell.Offset(0, 1).Value * 1) 'Is this even necessary? It's an identity.
End Select
Next
End With
End Sub
Screenshot:
Let us know if this helps.