Conditionally Coloring a Graph in Excel - vba

Hi there!
I am trying to color a graph (a task tracker) via VBA, in excel. The idea is to color all "categories" a certain color -- visually, it would mean making all bars on each "row" a particular color. I'm using the following code, which I copied from http://peltiertech.com/vba-conditional-formatting-of-charts-by-category-label/:
Sub ColorByCategoryLabel()
Dim rPatterns As Range
Dim iCategory As Long
Dim vCategories As Variant
Dim rCategory As Range
Set rPatterns = ActiveSheet.Range("A1:A5")
With ActiveChart.SeriesCollection(2)
vCategories = .XValues
For iCategory = 1 To UBound(vCategories)
Set rCategory = rPatterns.Find(What:=vCategories(iCategory))
.Points(iCategory).Format.Fill.ForeColor.RGB = rCategory.Interior.Color
Next
End With
End Sub
and I can't figure out what is wrong.
Basically, I have a series (series2), with horizontal (category) axis labels consisting of integers from 1-5. This category determines the vertical position of the bar, but I also want to color each bar in this series according to this vertical position, according to the color in the range(a1:a5) -- which is exactly what this code seems to be doing.
Any suggestions, as to the code, or perhaps, any alternative way to color bar graphs based on the value of the "horizontal (category) axis"?
Thanks!

Well, I found an answer to my problem by stepping through it. I can't imagine this to be the easiest way to horizontal bar graphs according to their height but it works.
Sub ColorByCategoryLabel()
Dim iCategory As Long
Dim vCategories As Variant
Dim rCategory As Range
Dim CurColor As Double
Dim CurColorIndex As Long
Dim CurHeight As Double
CurHeight = 0
CurColorIndex = 1
CurColor = ActiveSheet.Cells(CurColorIndex + 1, 10).Interior.Color
ActiveSheet.ChartObjects("Chart 1").Select
With ActiveChart.SeriesCollection(2)
vCategories = .XValues
For iCategory = 1 To UBound(vCategories)
If .Points(iCategory).Top > CurHeight Then
CurColorIndex = CurColorIndex + 1
CurColor = ActiveSheet.Cells(CurColorIndex + 1, 10).Interior.Color
CurHeight = .Points(iCategory).Top
End If
.Points(iCategory).Format.Fill.ForeColor.RGB = CurColor
Next
End With
End Sub
You would need to modify the line
Curcolor = ActiveSheet.Cells(CurColorIndex+1,10).Interior.Color
To properly specify the cells whose background color you wish to copy.
By the way, if anyone is interested in the timetracker, it is hosted here: https://drive.google.com/file/d/0B85fvjQDbl3lUVpPNmdGT1VkWW8/view?usp=sharing

Related

Excel Chart - How to draw discontinuous series in VBA without range reference

Excel 2010.
Issue : I need to plot a *single* *discontinuous* series in a XY-scatter chart *via VBA* without referencing a range in the sheet.
It is easy to achieve that when the Yvalues are laid-out in a sheet range, by inserting blank values at the discontinuities ; as long as one selects 'Show empty cells as: Gaps' in Select Data > Hidden and Empty Cells. Here is an example (the Series2 in red is the one that matters) :
So I was trying to reproduce the same via VBA :
Sub addDiscountinuousSingleSeries()
Dim vx As Variant, vy As Variant
Dim chrtObj As ChartObject, chrt As Chart, ser As Series
Set chrtObj = ActiveSheet.ChartObjects("MyChart"): Set chrt = chrtObj.Chart
Set ser = chrt.SeriesCollection.NewSeries
vx = Array(0.3, 0.3, 0.3, 0.7, 0.7, 0.7)
vy = Array(-1, 1, vbNullString, -1, 1, vbNullString)
'vy = Array(-1, 1, CVErr(xlErrNA), -1, 1, CVErr(xlErrNA)) 'doesn't work either
'vy = Range(RANGE_YVALUES_WITH_BLANK) 'this would work, but I do not want to reference a range
chrt.DisplayBlanksAs = xlNotPlotted 'VBA equivalent to 'Show empty cells as: Gaps'
With ser
ser.Name = "VBA Series"
.XValues = vx
.Values = vy
End With
End Sub
But the blank values in the vy array seems to be ignored and the two vertical bars are now connected, which I am trying to avoid (green series).
I know that I could delete the middle line programmatically, but in the real-life problem I am trying to solve it would not be the right solution (too complex, too slow).
My question : is there a way to specify the series' .Values array to get the expected behavior and get a gap between the two vertical green segments in vba (with only one series and no reference to a sheet range)?
You could just format the lines you don't want. Maybe not the prettiest way, but it'd achieve what your after.
ser.Points(3).Format.Line.Visible = msoFalse
ser.Points(4).Format.Line.Visible = msoFalse
ser.Points(6).Format.Line.Visible = msoFalse
Or:
For i = 1 To ser.Points.Count
If i <> 1 Then k = i - 1 Else k = i
If ser.Values(i) = 0 Or ser.Values(k) = 0 Then
ser.Points(i).Format.Line.Visible = msoFalse
End If
Next

VBA Excel: Different colors in one line diagram depending on value

I'm looking for a way to have three different colors in the same line chart of a diagram in Excel, depending on the values themselves or where they are from (from which sheet f.e).
Till now, I have the following code:
Sub ChangeColor()
Dim i As Integer
Dim IntRow As Integer
Dim r As Range
ActiveSheet.ChartObjects("Cash").Activate
ActiveChart.SeriesCollection(1).Select
IntRow = ActiveChart.ChartObjects("Cash").Count
For i = 2 To IntRow
Set r = Cells(2, i)
If r.Value < 3000 Then
Selection.Border.ColorIndex = 5
Else
Selection.Border.ColorIndex = 9
End If
Next
End Sub
However, the if statement is not considered and the color of the whole line changes only whenever I change the first ColorIndex. I have no idea, how to color parts of the line depending on the values in the underlying table.
Moreover, by defining IntRow as ActiveChart.ChartObjects("Cash").Count I'm not able to get the length of my array. This problem can be solved by manual counting and declaring IntRow as an Integer, however, the version above seems nicer (if that is possible of course).
I appreciate any help! Thank you.
Alexandra
You can read the values directly from the chart series:
Sub ChangeColor()
Dim cht As Chart, p As Point, s As Series
Dim i As Integer
Dim numPts As Long
'access the chart directly - no select/activate required
Set cht = ActiveSheet.ChartObjects("Cash").Chart
'reference the first series
Set s = cht.SeriesCollection(1)
'how many points in the first series?
numPts = s.Points.Count
'loop over the series points
For i = 1 To numPts
Set p = cht.SeriesCollection(1).Points(i)
p.Border.ColorIndex = IIf(s.Values(i) < 3000, 5, 9)
Next
End Sub

VBA: need to sort shapes

Recently, in an interview I encountered a question in VBA. The question is:
Write a program to sort the shapes in a worksheet, like for example : I have various shapes like circle, triangle, rectangle, pentagon... This needs to be sorted and placed one below the other.
I tried with Shapes object and msoshapeRectangle method. But it didnt work.
Could you please tell me is this possible to be done?
Thanks
It was an interesting challenge, so I did it. Might as well post the result (commented for clarity):
Sub tgr()
'There are 184 total AutoShapeTypes
'See here for full list
'https://msdn.microsoft.com/VBA/Office-Shared-VBA/articles/msoautoshapetype-enumeration-office
Dim aShapeTypes(1 To 184) As String
Dim ws As Worksheet
Dim Shp As Shape
Dim i As Long, j As Long
Dim vShpName As Variant
Dim dLeftAlign As Double
Dim dTopAlign As Double
Dim dVerticalInterval As Double
Dim dHorizontalInterval As Double
Dim dPadding As Double
Set ws = ActiveWorkbook.ActiveSheet
'Sort order will be by the AutoShapeType numerical ID
'Using this, shapes will be sorted in this order (incomplete list for brevity):
' Rectangle, Parallelogram, Trapezoid, Diamond, Rounded rectangle, Octagon, Isosceles triangle, Right triangle, Oval, Hexagon
'Note that you can use a Select Case to order shapes to a more customized list
'I use this method to put the -2 (indicates a combination of the other states) at the bottom of the sort order
For Each Shp In ws.Shapes
Select Case Shp.AutoShapeType
Case -2: aShapeTypes(UBound(aShapeTypes)) = aShapeTypes(UBound(aShapeTypes)) & "||" & Shp.Name
Case Else: aShapeTypes(Shp.AutoShapeType) = aShapeTypes(Shp.AutoShapeType) & "||" & Shp.Name
End Select
Next Shp
'Now that all shapes have been collected and put into their sort order, perform the actual sort operation
'Adjust the alignment and vertical veriables as desired
'The Padding variable is so that the shapes don't start at the very edge of the sheet (can bet set to 0 if that's fine)
'I have it currently set to sort the shapes vertically, but they can be sorted horizontally by uncommenting those lines and commenting out the vertical sort lines
dPadding = 10
dLeftAlign = 5
dTopAlign = 5
dVerticalInterval = 40
dHorizontalInterval = 40
j = 0
For i = LBound(aShapeTypes) To UBound(aShapeTypes)
If Len(aShapeTypes(i)) > 0 Then
For Each vShpName In Split(Mid(aShapeTypes(i), 3), "||")
With ws.Shapes(vShpName)
'Vertical Sort
.Left = dLeftAlign
.Top = j * dVerticalInterval + dPadding
'Horizont Sort
'.Top = dTopAlign
'.Left = j * dHorizontalInterval + dPadding
End With
j = j + 1
Next vShpName
End If
Next i
End Sub

How do I determine end points of a Line drawing object?

I have a line (=autoshape) drawing object on an Excel spreadsheet. I want to determine which cell it "points" to. For this I need to know the coordinates of the start and end points.
I can use .Top, .Left, .Width, .Height to determine bounding rectangle, but the line may be in 2 different positions in that rectangle.
To do this you must use the members HorizontalFlip and VerticalFlip. The following function should do what you want:
Function CellFromArrow(ByVal s As Shape) As Range
Dim hFlip As Integer
Dim vFlip As Integer
hFlip = s.HorizontalFlip
vFlip = s.VerticalFlip
Select Case CStr(hFlip) & CStr(vFlip)
Case "00"
Set CellFromArrow = s.BottomRightCell
Case "0-1"
Set CellFromArrow = Cells(s.TopLeftCell.Row, s.BottomRightCell.Column)
Case "-10"
Set CellFromArrow = Cells(s.BottomRightCell.Row, s.TopLeftCell.Column)
Case "-1-1"
Set CellFromArrow = s.TopLeftCell
End Select
End Function
This code is tested in Excel 2010. Seems to work. Hope this helps!
EDIT:
If you have to worry about shapes contained in groups, then it seems the only solution is to ungroup, iterate through the shapes and then regroup. Something like the following:
Dim s As Shape
For Each s In ActiveSheet.Shapes
If s.Type = msoGroup Then
Dim oldName as String
Dim sGroup As GroupShapes
Dim GroupMember as Shape
Set sGroup = s.GroupItems
oldName = s.Name 'To preserve the group Name
s.Ungroup
For Each GroupMember in sGroup
'DO STUFF
Next
Set s = sGroup.Range(1).Regroup 'We only need to select one shape
s.Name = oldName 'Rename it to what it used to be
End If
Next
You can refer to ShapeRange Documentation for more info on the Regroup method.
Let me know if this works for you!

Elegant way to highlight chart data series in Excel

I want to outline the chart data range source(s) in a table, in much the same way that the GUI will outline a range in blue if the chart data series is clicked. The user can choose various chart views and the range highlight colours for each data series need to match those displayed in the chart.
For the record, here are the methods I considered:
Parse the chart series values string and extract the data range
Do a lookup on a table that stores information on the ranges and the colours to be used
In the end I went with option 2 as is seemed easier to implement and to properly manage the colours I would probably have to store them for method 1 anyway, negating its benefits.
The highlight procedure is called from the Worksheet_Change event, a lookup is done on the chart name, the ranges and colours pulled from the table and then the cell formatting is carried out. The limitation of this method is that the range/colour data for each new chart view must be pre-calculated. This isn't much of a problem for my current implementation, but my be a limiting factor in future use where the charts might be more dynamic.
So although I've got a version of this working fine, I'm sure there must be a more elegant way of achieving this.
Any suggestions?
Edit:
OK, this seems to handle more cases better. The triggering code is the same, but here is new code for the module:
Function SeriesRange(s As Series) As Range
Dim sf As String, fa() As String
sf = s.Formula
sf = Replace(sf, "=SERIES(", "")
If sf = "" Then
Set SeriesRange = Nothing
Exit Function
End If
fa = Split(sf, ",")
Set SeriesRange = Range(fa(2))
End Function
Sub x(c As Chart)
Dim sc As Series
Dim sr As Range
If SeriesRange(c.SeriesCollection(1)) Is Nothing Then
Exit Sub
End If
Set sr = SeriesRange(c.SeriesCollection(1))
sr.CurrentRegion.Interior.ColorIndex = xlNone
For Each sc In c.SeriesCollection
If sc.Interior.Color > 1 Then
SeriesRange(sc).Interior.Color = sc.Interior.Color
ElseIf sc.Border.ColorIndex > 1 Then
SeriesRange(sc).Interior.Color = sc.Border.Color
ElseIf sc.MarkerBackgroundColorIndex > 1 And sc.MarkerBackgroundColorIndex < 57 Then
SeriesRange(sc).Interior.ColorIndex = sc.MarkerBackgroundColorIndex
ElseIf sc.MarkerForegroundColorIndex > 1 And sc.MarkerForegroundColorIndex < 57 Then
SeriesRange(sc).Interior.ColorIndex = sc.MarkerForegroundColorIndex
Else
MsgBox "Unable to determine chart color for data series " & sc.Name & " ." & vbCrLf _
& "It may help to assign a color rather than allowing AutoColor to assign one."
End If
Next sc
End Sub
/Edit
This is probably more barbaric than elegant, but I think it does what you want. It involves your first bullet point to get the range from the Series object, along with a sub to run through all the Series objects in the SeriesCollection for the chart. This is activated on Chart_DeActivate. Most of this code is jacked - see comments for sources.
In a module:
Function SeriesRange(s As Series) As Range
Dim sf As String, fa() As String
Dim i As Integer
Dim result As Range
sf = s.Formula
sf = Replace(sf, "=SERIES(", "")
fa = Split(sf, ",")
Set SeriesRange = Range(fa(2))
End Function
Sub x(c As Chart)
Dim sc As Series
Dim sr As Range
Set sr = SeriesRange(c.SeriesCollection(1))
sr.CurrentRegion.Interior.ColorIndex = xlNone
For Each sc In c.SeriesCollection
SeriesRange(sc).Interior.Color = sc.Interior.Color
Next sc
End Sub
In the ThisWorkbook object module:
' Jacked from C Pearson http://www.cpearson.com/excel/Events.aspx '
Public WithEvents CHT As Chart
Private Sub CHT_Deactivate()
x CHT
End Sub
Private Sub Workbook_Open()
Set CHT = Worksheets(1).ChartObjects(1).Chart
End Sub
Have you tried using Conditional Formatting?