Multiple charts by groups in column VBA - vba

m completely new to vba programming and I was unable to find similar questions. Please guide me in the right direction if the answer to a similar problem is already to be found in this forum.
I have the following dataset (in practice, the dataset contains more names and dates per name):
Name Date Value1 Value2
AA 01-02-2022 0.5744 10
AA 01-03-2022 0.5542 10
AA 01-04-2022 0.5551 10
AA 01-05-2022 0.5678 10
BB 01-02-2022 0.5518 11
BB 01-03-2022 0.5659 11
BB 01-04-2022 0.5455 11
BB 01-05-2022 0.5404 11
CC 01-02-2022 0.5524 12
CC 01-03-2022 0.5321 12
CC 01-04-2022 0.5554 12
CC 01-05-2022 0.5407 12
I want to create multiple charts using VBA - separate charts for each name in column "Name" i.e. one chart for AA, another chart for BB and a third chart for CC etc.VBA The charts should plot Value1 and Value2 on the Y-axis against Date on the X-axis.
The data is loaded into the spreadsheet using a power query, which extracts data from an Oracle database.
If possible, I would like to place the charts next and/or appended to each other.
As (new) names are regularly removed (added) to the dataset, I am looking for a dynamic solution and hope to solve this using VBA.
Please do not hesitate to comment if I need be more clear in my description or to elaborate.
Kind regards,

Please, try the next code. It uses a dictionary to keep the unique names as key and range first row, respectively, last row as items. Then builds DataSource based on them:
Sub TestInsertCClusteredChart()
Dim sh As Worksheet, lastR As Long, arr, arrIt, i As Long, dict As Object
Dim rngDS As Range, ch_shape As Shape, chLeft As Double, chTop As Double
Set sh = ActiveSheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row
arr = sh.Range("A1:A" & lastR).Value2
Set dict = CreateObject("Scripting.Dictionary")
'dict.RemoveAll
For i = 2 To UBound(arr)
If Not dict.Exists(arr(i, 1)) Then
dict.Add arr(i, 1), Array(i)
Else
arrIt = dict(arr(i, 1))
If UBound(arrIt) = 0 Then
ReDim Preserve arrIt(1)
arrIt(1) = i
Else
arrIt(1) = i
End If
dict(arr(i, 1)) = arrIt
End If
Next i
chLeft = sh.Range("F2").left: chTop = sh.Range("F2").top 'positions of the first chart
'build dataSource range and insert chart:
For i = 0 To dict.count - 1
Set rngDS = Union(sh.Range("A1:D1"), sh.Range(sh.cells(dict.Items()(i)(0), "A"), sh.cells(dict.Items()(i)(1), "D")))
Set ch_shape = sh.Shapes.AddChart2 'insert the chart
With ch_shape.Chart
With .ChartArea
.left = chLeft
.top = chTop
End With
.ChartType = xlColumnClustered
.SetSourceData rngDS
chLeft = chLeft + .ChartArea.width 'calculate the left position for the next chart
End With
Next i
End Sub
Sub deleteCharts(sh As Worksheet)
Dim s As Shape
For Each s In sh.Shapes
If s.HasChart Then s.Delete
Next
End Sub
Please, send some feedback after testing it.
The chart is created using default dimensions (height, width). They can be set, of course.
Adapted the code according to your recent requirements.

Related

Comparing Snapshot Data in Excel (VBA?) [duplicate]

I have this worksheet which gets data from API and its refreshes itself every 200 milliseconds. I want to calculate the change in value which is constantly increasing every 200 ms. For example Cell B2 has a value of 4 after 200 ms its changes to 7 then to 16 then to 26 etc, it just keeps adding value into it. All I want is to subtract the old value from the latest value to get the change for example 7-4=3 or 16-7=9 or 26-16=10.
I have added an image for clarification. This shows how I'm getting a data from software.
And one more image:
First, enable Iterative Calculations in Excel by going to File -> Options -> Formulas and then checking the box next to "Enable iterative calculation".
You need to define the following cells:
cell B1 0 (set to 1 to reset)
cell B2 =IF($B$1 = 1,, $B$2 + 1)
Use the following formula and fill down from B9 for as many changes as you would like to see (This formula assumes you have maximum iterations set to 100):
cell B9 =IF($B$1 = 1,"", IF($B$2 / 100 = $A9, $B$5, B9))
I will try to show an example here. If your cell that automatically updates is B5, then the changes will be tracked in B9 and below as the cell is refreshed. It may not be exactly what you are looking for, but I think it is close.
A B
1 reset 0
2 count 500
3
4
5 price 9
6
7
8 ID price
9 1 11
10 2 12
11 3 13
12 4 12
13 5 9
I suggest VBA solution, based on worksheet change event handling. Open VBA Project and put the below code into the target worksheet in Microsoft Excel Objects section:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
' add reference to Microsoft Scripting Runtime via Menu - Tools - References
Const Scope = "C2:C5" ' monitoring area
Const DX = 1 ' horizontal result offset
Const DY = 0 ' vertical result offset
Const Buf = 0 ' FIFO buffer size
Static oData(0 To Buf) As New Dictionary
Static oIndex As New Dictionary
Dim rCells As Range
Dim oCell
Dim i As Long
Set rCells = Application.Intersect(Target, Target.Parent.Range(Scope))
If Not rCells Is Nothing Then
For Each oCell In rCells
With oCell
i = oIndex(.Address)
.Offset(DY, DX).Value = .Value - oData(i)(.Address)
oData(i)(.Address) = .Value
i = i + 1
If i > Buf Then i = 0
oIndex(.Address) = i
End With
Next
End If
End Sub
I added some comments for constants. Set the range which change to be monitored in Scope, the offsets where the resulting delta will be output in DX and DY, and as a bonus that algorithm supports computing delta not only between last and previous numbers, but also between any number of frames for each target cell via buffer organized as array of dictionaries, so set the size of the buffer in Buf, if you do not want to use the buffer then just leave 0 size, e. g. the value of 3 will compute delta between the last value and the one delayed by 800 ms for your case.
UPDATE
There is slightly simplified version of the code as requested in comment, put the below code into the target worksheet:
Private Sub Worksheet_Change(ByVal Target As Range)
Const Scope = "C2:C5" ' monitoring area
Static oData As New Dictionary
Dim rCells As Range
Dim oCell
Dim dDelta
Set rCells = Application.Intersect(Target, Target.Parent.Range(Scope))
If Not rCells Is Nothing Then
For Each oCell In rCells
With oCell
dDelta = .Value - oData(.Address)
If dDelta <> 0 Then
.Offset(0, 1).Value = dDelta
oData(.Address) = .Value
End If
End With
Next
End If
End Sub

Incrementing VBA For loop through command button

Good day!
I am currently having a slight issue with a command button, which, I would like to be able to do the following: format a specific row to a certain row height, add thick borders to a certain number of cells in this same row and counting and adding the number of rows thus produced to the initial number in the file. Basically, the button should enable the user of the spread sheet to add a new row with specific formatting into which the user will input data and keep track of the number of rows added.
My current code stands as is:
Option Explicit
Private Sub NewLineRedButton_Click()
Dim i As Long
Dim y As Long
For y = ThisWorkbook.Worksheets("Flags").Cells(16.3) To y + 1
ThisWorkbook.Worksheets("Flags").Cells(16, 3) = y + 1
For i = 20 To i + y Step 1
ThisWorkbook.Worksheets("Flags").Rows(i).RowHeight = 45
ThisWorkbook.Worksheets("Flags").Cells(i, 1).Borders.LineStyle = xlContinuous
ThisWorkbook.Worksheets("Flags").Cells(i, 1).Borders.Weight = xlMedium
Next
Next
End Sub
At the moment the code executes only for two rows below and stops. I am not quite sure, why...?
Writing a for loop like this
For y = ThisWorkbook.Worksheets("Flags").Cells(16.3) To y + 1
is the same as writing it like this
For y = ThisWorkbook.Worksheets("Flags").Cells(16.3) To 1
I'm guessing the value in the cell is zero so it will execute the loop for 0 and 1 - i.e. the two times you are seeing.
You need something like
lEndRow = lStartRow + (lRowCount - 1)
For y = lStartRow to lEndRow
I have this going fine now. Just can't get it to do all the columns with the right borders... I am not sure how to to call them while using the cells(#, #) notation and I can't see how to use the Range(''Z#,Z#'') notation with my i variable being a Long...
Anyways: here's the result so far:
Option Explicit
Private Sub NewLineRedButton_Click()
Dim i As Long
Dim y As Long
Dim RowEnd As Long
RowEnd = ThisWorkbook.Worksheets("Flags").Cells(Rows.Count, 1).End(xlUp).Row
For y = 19 To RowEnd
ThisWorkbook.Worksheets("Flags").Cells(16, 3) = y - 17 ' First row which is already on the sheet is on row 19, first row appearing by button clicking is on row 20 and the program is counting the header hence the y - 17 for the value of the number of rows.
For i = 19 To RowEnd + 1 Step 1
ThisWorkbook.Worksheets("Flags").Rows(i).RowHeight = 45
ThisWorkbook.Worksheets("Flags").Cells(i, 1).Borders.LineStyle = xlContinuous
ThisWorkbook.Worksheets("Flags").Cells(i, 1).Borders.Weight = xlMedium
Next
Next
End Sub
Thanks for the help and ideas, finally wiggled around and found other resources from the leads given here.

Excel 2010 VBA - Storing decimal in array and using to chart percentage of stored value

SO Community,
I'm looking to automate some of the metrics that are used at my work using VBA. I am currently trying to read through an array that I have my ticketing raw data stored in and then either store the value as a decimal or percentage. After this is stored in the array, I am attempting to create or update a chart series with the array and display this value as a percent. I suspect that I'm just missing some syntax for this, but I have checked SO, MSDN and Excel help and have had no luck. I have attached the relevant code below:
FUNCTION
Function calcTopApplications(iArray As Variant)
Dim m_counter As Long, r_counter As Long, placeholder As Double
Dim fNav_counter As Long, pmoNav_counter As Long, rmgr_counter As Long, wlm_counter As Long, total_counter As Long
ReDim tkt_month_arr(12), tkt_fnav_arr(12)
For m_counter = 1 To 12
fNav_counter = 0
pmoNav_counter = 0
rmgr_counter = 0
wlm_counter = 0
total_counter = 0
For r_counter = 2 To UBound(iArray, 1)
If iArray(r_counter, 1) <> iArray(r_counter - 1, 1) Then
If CDate(iArray(r_counter, 5)) >= DateAdd("m", -m_counter, DateSerial(Year(Date), Month(Date), 1)) Then
If CDate(iArray(r_counter, 5)) < DateAdd("m", (1 - m_counter), DateSerial(Year(Date), Month(Date), 1)) Then
total_counter = total_counter + 1
If StrConv(iArray(r_counter, 7), vbLowerCase) = "franchise navigator" Then
fNav_counter = fNav_counter + 1
End If
End If
End If
End If
Next r_counter
placeholder = FormatNumber(fNav_counter / total_counter, 2)
tkt_month_arr(12 - (m_counter - 1)) = CLng(DateAdd("m", -m_counter, DateSerial(Year(Date), Month(Date), 1)))
tkt_fnav_arr(12 - (m_counter - 1)) = placeholder
Next m_counter
End Function
SUBROUTINE
If Me.ChartObjects("Top 4 Applications Ticket Volume") Is Nothing Then
On Error GoTo 0
With Me.Shapes.AddChart
.Left = Me.Range("A16").Left
.Top = Me.Range("A16").Top
.Width = Me.Range("A16:S16").Width
.Height = Me.Range("A16:A30").Height
.Select
End With
With ActiveChart
.ChartType = xlLine
.ChartStyle = 42
.HasDataTable = True
.HasTitle = True
.Parent.Name = "Top 4 Applications Ticket Volume"
.ChartTitle.Caption = "Open/Close Ticket Volume by Month (Top 4 Applications)"
.Axes(xlCategory).TickLabels.NumberFormat = "mmm yyyy"
End With
With ActiveChart.SeriesCollection
.NewSeries
.Item(1).Name = "Franchise Navigator"
.Item(1).XValues = tkt_month_arr
.Item(1).Values = tkt_fnav_arr
End With
Else
With Me.ChartObjects("Top 4 Applications Ticket Volume")
.Select
End With
With ActiveChart.SeriesCollection
.Item(1).XValues = tkt_month_arr
.Item(1).Values = tkt_fnav_arr
End With
End If
This gives me the values for the percentage but does not behave on the chart as a percentage (have the % symbol).
When you create a chart "normally" and add a data table, all you have to do is format the source cells, and the table follows. But the method you use means you don't have "source cells"; and as far as I was able to tell, Microsoft doesn't provide a method to modify the formatting of the data table when it's generated with your method.
An "ugly workaround" is to create a hidden sheet, put the data there, and format it correctly. Point to this data when you create the data table, and the formatting will be correct.
It would be nice if Microsoft provided the flexibility you are asking for... sigh

VBA excel finding the statistical mode of a collection

So I'm trying to analysis some data in Excel and having some trouble finding the most frequent numbers. I have an unknown number of locations which can have an unknown number of donations. For example
Brantford $50.00
Brantford $25.00
Brantford $50.00
Windsor $200.00
Quebec $25.00
Quebec $100.00
Quebec $50.00
Quebec $50.00
Quebec $25.00
Quebec $50.00
Quebec $50.00
Quebec $25.00
Quebec $100.00
Quebec $40.00
Windsor $140.00
Windsor $20.00
Windsor $20.00
So I need to use VBA to find for each location the count, sum, mean and mode (has to be done through VBA, can't just write instructions on how to do this using advanced filters/pivot tables :().
So right now using VBA I have a dictionary object that stores the location name as a key and each donation in a collection. Using the count of the collection I have the count, can easily loop through the collection for the sum, using those I have the mean; but, I am not sure the most efficient way to get the mode.
I know I can find it if my data was in an array using Application.mode, but that doesn't appear to work for collections :(. Converting a collection to an array though to find the mode though really doesn't strike me as the most efficient solution. Only other option i can find of though is to sort the collections then loop through them to find the mode.
So wondering if anyone knows of a good way to find the statistical mode of a collection?
Dim locdata As Object
Set locdata = CreateObject("scripting.dictionary")
For counter = 2 To max
mykey = Cells(counter, loccol).value
If Not (locdata.exists(mykey)) Then
locdata.Add (mykey), New Collection
End If
locdata(mykey).Add (Cells(counter, donamountcol).value)
Next counter
For Each k In locdata.keys
locname = k
Cells(counter, 1) = k
Cells(counter, 2) = locdata(k).Count
donationtotal = 0
For Each donvalue In locdata(k)
donationtotal = donationtotal + donvalue
Next donvalue
Cells(counter, 3) = donationtotal
Cells(counter, 4) = donationtotal / CDbl(locdata(k).Count)
'Cells(counter, 5) = Application.mode(locdata(k)) doesn't work :(
counter = counter + 1
Next k
edit: Ideally the output should be (using Quebec as an example)
Quebec: Count: 10 Sum: 515 Average: 51.5 Mode: 50
Here is how you can have values in a range into aarray dynamically. And I would use CountIF in the VBA to find the most frequent objects by their names.. Since you don't know the location names or the donations. Then array is the way to go.
Dim ar as Variant
Dim endRow as Long
'get last row in the range
endRow = Sheets(1).Cells(Sheets(1).Rows.Count, "A").End(xlUp).Row
'ar = WorksheetFunction.Transpose(Shets(1).Range("A1:A12")
'using endrow
ar = WorksheetFunction.Transpose(Shets(1).Range("A1").resize(endRow).value)
UPDATE: The subroutine below uses one iteration (for loop) to find the Mode..
Sub FrequencyByLocDonations()
Dim ar As Variant, dc As Object
Dim rngInput As Range, mxRng As Range
Dim endRow As Long, i As Integer
Dim counts As Double, maxLoc As Double
Dim maxLocation As String
Set dc = CreateObject("Scripting.Dictionary")
'-- When you know the range
' ar = WorksheetFunction.Transpose(Shets(1).Range("A1:A12").Value
'get last row in the range when you don't know but the starting cell
endRow = Sheets(3).Cells(Sheets(3).Rows.Count, "C").End(xlUp).Row
Set rngInput = Sheets(3).Range("C2").Resize(endRow - 1, 1)
'--you may also use that set rngInput as well
' WorksheetFunction.Transpose(rngInput).Value
'-- using endrow-1 to not to take an extra blank row at the end
ar = WorksheetFunction.Transpose(Sheets(3).Range("C2").Resize(endRow - 1, 2).Value)
For i = LBound(ar, 2) To UBound(ar, 2)
If Not (dc.exists(ar(1, i))) Then
counts = Application.WorksheetFunction.CountIf(rngInput, ar(1, i))
If counts >= maxLoc Then
maxLocation = ar(1, i)
maxLoc = counts
End If
dc.Add ar(1, i), counts
End If
Next i
'-- output to the Sheet
Sheets(3).Range("C2").Offset(0, 2).Resize(UBound(dc.keys) + 1, 1) = _
Application.Transpose(dc.keys)
Sheets(3).Range("C2").Offset(0, 3).Resize(UBound(dc.items) + 1, 1) = _
Application.Transpose(dc.items)
Sheets(3).Range("C2").Offset(0, 4) = "Most Frequent Location :" _
& maxLocation & "; " & maxLoc
Set dc = Nothing
End Sub
output:
I have run into a similar situation in the past. It seemed to me there was a very powerful VBA function missing from excel - equivalent to the "where" statement in MySQL.
So I wrote a very simple one myself... This lacks a lot of functionality, but it will allow you to do what you are asking for, while minimizing the amount of code you write.
Fundamental concept: you can return an array from a function call, and Excel built-in functions can operate on such an array as they would on a function. Thus, if you have a function that returns "all the numbers of which I want the mode", then =MODE(myfunction()) will give you the answer you want.
I chose to call my function subset(criteria, range1, range2).
In its most simple form it returns the elements in range2 that correspond to elements in range1 that meet criteria.
This is NOT extensively tested, but I hope you get the idea.
By the way, you can enter this as an array formula (shift-ctrl-enter) in multiple cells; in that case you get the first returned element in the first cell, etc. Sometimes that's a useful trick when you have a function that needs to return more than one value (e.g. a range) - but for this case you only need the result to feed to another function.
Option Explicit
' Function subset(criteria, range1, range2)
' Return an array with the elements in range2 that correspond to
' elements in range1 that match "criteria"
' where "criteria" can be a string, or a value with a < = > sign in front of it
' example: =subset("bravo", A1:A10, B1:B10)
' returns all cells from B that corresponds to cells in A with "bravo"
' =subset("<10", A1:A10, B1:B10) returns all cells in B corresponding to
' cells in A with a value < 10
' This is analogous to the "where" function in SQL, but much more primitive
Function subset(criteria As String, range1 As Range, range2 As Range)
Dim c
Dim result
Dim ii, jj As Integer
On Error GoTo etrap
If range1.Cells.Count <> range2.Cells.Count Then Exit Function
ReDim result(1 To range1.Cells.Count)
ii = 1
jj = 1
For Each c In range1.Cells
If compare(c.Value, criteria) = 0 Then
result(ii) = range2.Cells(jj).Value
ii = ii + 1
End If
jj = jj + 1
Next c
If ii > 1 Then
ReDim Preserve result(1 To ii - 1)
subset = result
Else
subset = Nothing
End If
Exit Function
etrap:
MsgBox "Error " & Err.Description
End Function
Private Function compare(a, b)
' type of a decides what kind of comparison we do
If TypeName(a) <> TypeName("hello") Then
' use numerical comparison
compare = Not (Evaluate(a & b))
Else
' use string comparison
compare = StrComp(a, b, vbTextCompare)
End If
End Function
I actually just decided to make a dictionary of dictionaries. So I have the locations and each location than has a dictionary of the count of each donation amount. Was easy enough to compare counts that way to find the mode.

How to "flatten" or "collapse" a 2D Excel table into 1D?

I have a two dimensional table with countries and years in Excel. eg.
1961 1962 1963 1964
USA a x g y
France u e h a
Germany o x n p
I'd like to "flatten" it, such that I have Country in the first col, Year in the second col, and then value in the third col. eg.
Country Year Value
USA 1961 a
USA 1962 x
USA 1963 g
USA 1964 y
France 1961 u
...
The example I present here is only a 3x4 matrix, but the real dataset i have is significantly larger (roughly 50x40 or so).
Any suggestions how I can do this using Excel?
You can use the excel pivot table feature to reverse a pivot table (which is essentially what you have here):
Good instructions here:
http://spreadsheetpage.com/index.php/tip/creating_a_database_table_from_a_summary_table/
Which links to the following VBA code (put it in a module) if you don't want to follow the instructions by hand:
Sub ReversePivotTable()
' Before running this, make sure you have a summary table with column headers.
' The output table will have three columns.
Dim SummaryTable As Range, OutputRange As Range
Dim OutRow As Long
Dim r As Long, c As Long
On Error Resume Next
Set SummaryTable = ActiveCell.CurrentRegion
If SummaryTable.Count = 1 Or SummaryTable.Rows.Count < 3 Then
MsgBox "Select a cell within the summary table.", vbCritical
Exit Sub
End If
SummaryTable.Select
Set OutputRange = Application.InputBox(prompt:="Select a cell for the 3-column output", Type:=8)
' Convert the range
OutRow = 2
Application.ScreenUpdating = False
OutputRange.Range("A1:C3") = Array("Column1", "Column2", "Column3")
For r = 2 To SummaryTable.Rows.Count
For c = 2 To SummaryTable.Columns.Count
OutputRange.Cells(OutRow, 1) = SummaryTable.Cells(r, 1)
OutputRange.Cells(OutRow, 2) = SummaryTable.Cells(1, c)
OutputRange.Cells(OutRow, 3) = SummaryTable.Cells(r, c)
OutputRange.Cells(OutRow, 3).NumberFormat = SummaryTable.Cells(r, c).NumberFormat
OutRow = OutRow + 1
Next c
Next r
End Sub
-Adam
In Excel 2013 need to follow next steps:
select data and convert to table (Insert -> Table)
call Query Editor for table (Power Query -> From Table)
select columns that contain years
in context menu select 'Unpivot Columns'-command.
Support Office: Unpivot columns (Power Query)
In Excel 2016, Power Query is called Get & Transform and it is found in the Data tab.
#Adam Davis's answer is perfect, but just in case you're as clueless as I am about Excel VBA, here's what I did to get the code working in Excel 2007:
Open the workbook with the Matrix that needs to be flattened to a table and navigate to that worksheet
Press Alt-F11 to open the VBA code editor.
On the left pane, in the Project box, you'll see a tree structure representing the excel objects and any code (called modules) that already exist. Right click anywhere in the box and select "Insert->Module" to create a blank module file.
Copy and paste #Adman Davis's code from above as is into the blank page the opens and save it.
Close the VBA editor window and return to the spreadsheet.
Click on any cell in the matrix to indicate the matrix you'll be working with.
Now you need to run the macro. Where this option is will vary based on your version of Excel. As I'm using 2007, I can tell you that it keeps its macros in the "View" ribbon as the farthest right control. Click it and you'll see a laundry list of macros, just double click on the one called "ReversePivotTable" to run it.
It will then show a popup asking you to tell it where to create the flattened table. Just point it to any empty space an your spreadsheet and click "ok"
You're done! The first column will be the rows, the second column will be the columns, the third column will be the data.
Flattening a data matrix (aka Table) can be accomplished with one array formula¹ and two standard formulas.
      
The array formula¹ and two standard formulas in G3:I3 are is,
=IFERROR(INDEX(A$2:A$4, MATCH(0, IF(COUNTIF(G$2:G2, A$2:A$4&"")<COUNT($1:$1), 0, 1), 0)), "")
=IF(LEN(G3), INDEX($B$1:INDEX($1:$1, MATCH(1E+99,$1:$1 )), , COUNTIF(G$3:G3, G3)), "")
=INDEX(A:J,MATCH(G3,A:A,0),MATCH(H3,$1:$1,0))
Fill down as necessary.
While array formulas can negatively impact performance due to their cyclic calculation, your described working environment of 40 rows × 50 columns should not overly impact performance with a calculation lag.
¹ Array formulas need to be finalized with Ctrl+Shift+Enter↵. Once entered into the first cell correctly, they can be filled or copied down or right just like any other formula. Try and reduce full-column references to ranges more closely representing the extents of your actual data. Array formulas chew up calculation cycles logarithmically so it is good practise to narrow the referenced ranges to a minimum. See Guidelines and examples of array formulas for more information.
For anyone who wants to use the PivotTable to do this and is following the below guide:
http://spreadsheetpage.com/index.php/tip/creating_a_database_table_from_a_summary_table/
If you want to do it in Excel 2007 or 2010 then you first need to enable the PivotTable Wizard.
To find the option you need to go to "Excel Options" via the Main Excel Window icon, and see the options selected in the "customize" section, then select "Commands Not in the Ribbon" from the "Choose Commands from:" dropdown and "PivotTable and PivotChart Wizard" needs to be added to the right.. see the image below.
Once that is done there should be a small pivottable wizard icon in the quickbar menu at the top of the Excel window, you can then follow the same process as shown in the link above.
I developed another macro because I needed to refresh the output table quite often (input table was filled by other) and I wanted to have more info in my output table (more copied column and some formulas)
Sub TableConvert()
Dim tbl As ListObject
Dim t
Rows As Long
Dim tCols As Long
Dim userCalculateSetting As XlCalculation
Dim wrksht_in As Worksheet
Dim wrksht_out As Worksheet
'##block calculate and screen refresh
Application.ScreenUpdating = False
userCalculateSetting = Application.Calculation
Application.Calculation = xlCalculationManual
'## get the input and output worksheet
Set wrksht_in = ActiveWorkbook.Worksheets("ressource_entry")'## input
Set wrksht_out = ActiveWorkbook.Worksheets("data")'## output.
'## get the table object from the worksheet
Set tbl = wrksht_in.ListObjects("Table14") '## input
Set tb2 = wrksht_out.ListObjects("Table2") '## output.
'## delete output table data
If Not tb2.DataBodyRange Is Nothing Then
tb2.DataBodyRange.Delete
End If
'## count the row and col of input table
With tbl.DataBodyRange
tRows = .Rows.Count
tCols = .Columns.Count
End With
'## check every case of the input table (only the data part)
For j = 2 To tRows '## parse all row from row 2 (header are not checked)
For i = 5 To tCols '## parse all column from col 5 (first col will be copied in each record)
If IsEmpty(tbl.Range.Cells(j, i).Value) = False Then
'## if there is time enetered create a new row in table2 by using the first colmn of the selected cell row and cell header plus some formula
Set oNewRow = tb2.ListRows.Add(AlwaysInsert:=True)
oNewRow.Range.Cells(1, 1).Value = tbl.Range.Cells(j, 1).Value
oNewRow.Range.Cells(1, 2).Value = tbl.Range.Cells(j, 2).Value
oNewRow.Range.Cells(1, 3).Value = tbl.Range.Cells(j, 3).Value
oNewRow.Range.Cells(1, 4).Value = tbl.Range.Cells(1, i).Value
oNewRow.Range.Cells(1, 5).Value = tbl.Range.Cells(j, i).Value
oNewRow.Range.Cells(1, 6).Formula = "=WEEKNUM([#Date])"
oNewRow.Range.Cells(1, 7).Formula = "=YEAR([#Date])"
oNewRow.Range.Cells(1, 8).Formula = "=MONTH([#Date])"
End If
Next i
Next j
ThisWorkbook.RefreshAll
'##unblock calculate and screen refresh
Application.ScreenUpdating = True
Application.Calculate
Application.Calculation = userCalculateSetting
End Sub
VBA solution may not be acceptable under some situations (e.g. cannot embed macro due to security reasons, etc.). For these situations, and otherwise too in general, I prefer using formulae over macro.
I am trying to describe my solution below.
input data as shown in question (B2:F5)
column_header (C2:F2)
row_header (B3:B5)
data_matrix (C3:F5)
no_of_data_rows (I2) = COUNTA(row_header) + COUNTBLANK(row_header)
no_of_data_columns (I3) = COUNTA(column_header) + COUNTBLANK(column_header)
no_output_rows (I4) = no_of_data_rows*no_of_data_columns
seed area is K2:M2, which is blank but referenced, hence not to be deleted
K3 (drag through say K100, see comments description) = ROW()-ROW($K$2) <= no_output_rows
L3 (drag through say L100, see comments description) = IF(K3,IF(COUNTIF($L$2:L2,L2)
M3 (drag through say M100, see comments description) = IF(K3,IF(M2 < no_of_data_columns,M2+1,1),"-")
N3 (drag through say N100, see comments description) = INDEX(row_header,L3)
O3 (drag through say O100, see comments description) = INDEX(column_header,M3)
P3 (drag through say P100, see comments description) = INDEX(data_matrix,L3,M3)
Comment in K3: Optional: Check if expected no. of output rows has been achieved. Not required, if one only prepares this table limited to no. of output rows.
Comment in L3: Goal: Each RowIndex (1 .. no_of_data_rows) must repeat no_of_data_columns times. This will provide index lookup for row_header values. In this example, each RowIndex (1 .. 3) must repeat 4 times. Algorithm: Check how many times RowIndex has occurred yet. If it less than no_of_data_columns times, continue using that RowIndex, else increment the RowIndex. Optional: Check if expected no. of output rows has been achieved.
Comment in M3: Goal: Each ColumnIndex (1 .. no_of_data_columns) must repeat in a cycle. This will provide index lookup for column_header values. In this example, each ColumnIndex (1 .. 4) must repeat in a cycle. Algorithm: If ColumnIndex exceeds no_of_data_columns, restart the cycle at 1, else increment the ColumnIndex. Optional: Check if expected no. of output rows has been achieved.
Comment in R4: Optional: Use column K for error handling, as shown in column L and column M. Check if looked up value IsBlank to avoid incorrect "0" in the output because of blank input in data_matrix.
updated ReversePivotTable function so i can specify number of header columns and rows
Sub ReversePivotTable()
' Before running this, make sure you have a summary table with column headers.
' The output table will have three columns.
Dim SummaryTable As Range, OutputRange As Range
Dim OutRow As Long
Dim r As Long, c As Long
Dim lngHeaderColumns As Long, lngHeaderRows As Long, lngHeaderLoop As Long
On Error Resume Next
Set SummaryTable = ActiveCell.CurrentRegion
If SummaryTable.Count = 1 Or SummaryTable.Rows.Count < 3 Then
MsgBox "Select a cell within the summary table.", vbCritical
Exit Sub
End If
SummaryTable.Select
Set OutputRange = Application.InputBox(prompt:="Select a cell for the 3-column output", Type:=8)
lngHeaderColumns = Application.InputBox(prompt:="Header Columns")
lngHeaderRows = Application.InputBox(prompt:="Header Rows")
' Convert the range
OutRow = 2
Application.ScreenUpdating = False
'OutputRange.Range("A1:D3") = Array("Column1", "Column2", "Column3", "Column4")
For r = lngHeaderRows + 1 To SummaryTable.Rows.Count
For c = lngHeaderColumns + 1 To SummaryTable.Columns.Count
' loop through all header columns and add to output
For lngHeaderLoop = 1 To lngHeaderColumns
OutputRange.Cells(OutRow, lngHeaderLoop) = SummaryTable.Cells(r, lngHeaderLoop)
Next lngHeaderLoop
' loop through all header rows and add to output
For lngHeaderLoop = 1 To lngHeaderRows
OutputRange.Cells(OutRow, lngHeaderColumns + lngHeaderLoop) = SummaryTable.Cells(lngHeaderLoop, c)
Next lngHeaderLoop
OutputRange.Cells(OutRow, lngHeaderColumns + lngHeaderRows + 1) = SummaryTable.Cells(r, c)
OutputRange.Cells(OutRow, lngHeaderColumns + lngHeaderRows + 1).NumberFormat = SummaryTable.Cells(r, c).NumberFormat
OutRow = OutRow + 1
Next c
Next r
End Sub
Code with the claim for some universality
The book should have two sheets:
Sour = Source data
Dest = the "extended" table will drop here
Option Explicit
Private ws_Sour As Worksheet, ws_Dest As Worksheet
Private arr_2d_Sour() As Variant, arr_2d_Dest() As Variant
' https://stackoverflow.com/questions/52594461/find-next-available-value-in-excel-cell-based-on-criteria
Public Sub PullOut(Optional ByVal msg As Variant)
ws_Dest_Acr _
arr_2d_ws( _
arr_2d_Dest_Fill( _
arr_2d_Sour_Load( _
arr_2d_Dest_Create( _
CountA_rng( _
rng_2d_For_CountA( _
Init))))))
End Sub
Private Function ws_Dest_Acr(Optional ByVal msg As Variant) As Variant
ws_Dest.Activate
End Function
Public Function arr_2d_ws(Optional ByVal msg As Variant) As Variant
If IsArray(arr_2d_Dest) Then _
ws_Dest.Cells(1, 1).Resize(UBound(arr_2d_Dest), UBound(arr_2d_Dest, 2)) = arr_2d_Dest
End Function
Private Function arr_2d_Dest_Fill(Optional ByVal msg As Variant) As Variant
Dim y_Sour As Long, y_Dest As Long, x As Long
y_Dest = 1
For y_Sour = LBound(arr_2d_Sour) To UBound(arr_2d_Sour)
' without the first column
For x = LBound(arr_2d_Sour, 2) + 1 To UBound(arr_2d_Sour, 2)
If arr_2d_Sour(y_Sour, x) <> Empty Then
arr_2d_Dest(y_Dest, 1) = arr_2d_Sour(y_Sour, 1) 'iD
arr_2d_Dest(y_Dest, 2) = arr_2d_Sour(y_Sour, x) 'DTLx
y_Dest = y_Dest + 1
End If
Next
Next
End Function
Private Function arr_2d_Sour_Load(Optional ByVal msg As Variant) As Variant
arr_2d_Sour = ReDuce_rng(ws_Sour.UsedRange, 1, 0).Offset(1, 0).Value
End Function
Private Function arr_2d_Dest_Create(ByVal iRows As Long)
Dim arr_2d() As Variant
ReDim arr_2d(1 To iRows, 1 To 2)
arr_2d_Dest = arr_2d
arr_2d_Dest_Create = arr_2d
End Function
Public Function CountA_rng(ByVal rng As Range) As Double
CountA_rng = Application.WorksheetFunction.CountA(rng)
End Function
Private Function rng_2d_For_CountA(Optional ByVal msg As Variant) As Range
' without the first line and without the left column
Set rng_2d_For_CountA = _
ReDuce_rng(ws_Sour.UsedRange, 1, 1).Offset(1, 1)
End Function
Public Function ReDuce_rng(rng As Range, ByVal iRow As Long, ByVal iCol As Long) _
As Range
With rng
Set ReDuce_rng = .Resize(.Rows.Count - iRow, .Columns.Count - iCol)
End With
End Function
Private Function Init()
With ThisWorkbook
Set ws_Sour = .Worksheets("Sour")
Set ws_Dest = .Worksheets("Dest")
End With
End Function
'https://youtu.be/oTp4aSWPKO0