XYScatter Plot Incorrect XAxis when Drawn in Visual Basic? - vba

I'm trying to create a scatter plot using visual basic, the y axis being numerical values and the x axis being dates. The intention is for the plot to contain multiple series. Here is the relevant code:
ActiveWorkbook.Charts.Add
ActiveChart.ChartArea.Select
With ActiveChart
.ChartType = xlXYScatter
.HasTitle = True
.ChartTitle.Text = "Time Trend of Data"
.Axes(xlCategory, xlPrimary).HasTitle = True
.Axes(xlCategory, xlPrimary).AxisTitle.Characters.Text = "Dates"
.Axes(xlCategory, xlPrimary).CategoryType = xlTimeScale
.Axes(xlCategory, xlPrimary).TickLabels.NumberFormat = "m/d/yy;#"
.Axes(xlValue, xlPrimary).HasTitle = True
.Axes(xlValue, xlPrimary).AxisTitle.Characters.Text = "Total Time"
.Legend.Position = xlLegendPositionBottom
End With
ActiveSheet.Move After:=Sheets(ActiveWorkbook.Sheets.count)
ActiveSheet.Name = "Time Trend " + CStr(currTT) ' This is just to make sure the new sheet does not have the same name
After I generate some data, I attempt to plot it with a loop. The arrays I use are chartLabels - which is the name of each series, chartData - a 3d array with several data points for each series and xval and yval - arrays built from the chartData array which are plotted against each other:
For j = 0 To UBound(chartLabels)
If IsEmpty(chartLabels(j)) Then Exit For
Erase xval
Erase yval
ReDim Preserve xval(0 To 0)
ReDim Preserve yval(0 To 0)
xval(0) = chartData(1, j, 0)
yval(0) = chartData(2, j, 0)
For i = 0 To UBound(chartData, 3) - 1
If Not IsEmpty(chartData(2, j, i + 1)) Then
ReDim Preserve xval(0 To i + 1)
ReDim Preserve yval(0 To i + 1)
xval(i + 1) = chartData(1, j, i + 1)
yval(i + 1) = chartData(2, j, i + 1)
End If
Next
MsgBox (Join(xval, " || "))
ActiveChart.SeriesCollection.NewSeries
ActiveChart.SeriesCollection(j + 1).XValues = xval
ActiveChart.SeriesCollection(j + 1).Values = yval
ActiveChart.SeriesCollection(j + 1).Name = main.chartLabels(j)
Next
The MsgBox Statement is included to view the array that I am passing as the XValues to my Scatter Plot. The Output of the Array from the first series looks like:
All of these values are in date format. The number of series created varies. Also, the number of data points in each series can very depending on options that the user picks. However, the output that is generated on the scatter plot looks like:
Everything on the graph is correct except the xaxis is scaled to the position of each data point in its respective series, NOT the actual date (i.e. 1/0/00 is actually 0 in date format and 3/10/00 is 70 because there are about 70 data points).
I've tried using xlCategoryScale and xlAutomaticScale as my CategoryType. I've tried using CDate() on each of my xvals as well as CStr(). I've tried outputting different arrays for the XValues. Nothing works.
I have my suspicions that the problem is related to the fact that I'm trying to graph multiple series of data. But, if anybody could tell me the actual issue and/or a way around this issue, I would appreciate it very much. Thank you in advance!

I'm not sure why I didn't think of this... Two hours after I posted this I discovered that though using CStr() and CDate() on my XValues was useless, CDbl() actually worked. I'm going to refrain from just deleting my question because I don't think this is very intuitive. I'm not sure why this worked because I'm still formatting the xaxis as .CategoryType = xlTimeScale. I added this bit of code to the code above (this is the code I had tested before with CStr() and CDate(), but I did not include it in the question):
For k = 0 To UBound(xval)
xval(k) = CDbl(xval(k))
Next
This was added right before the MsgBox in the code from the question. This is the new output from the MsgBox for the XValues of the first series in the set:
Note: These values are all in the 42000's because each whole number is equal to a day and this data is from 2015.
2015 - 1900 = 115 years
115 years * 365.25 days/year = 42003.75
'In reality 2015 = 42005 (because of what years were leap years, etc.)
Finally, here is the actual output given the "Numerical" dates as xvalues in the scatter plot:
I guess the lesson here is that the xaxis likes numbers and using anything else could turn out to be a royal pain. If anyone comes across this post, I hope it was helpful!

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

Format datatable values in VBA

Currently working on a vba script that makes charts automatically. I would like to add a datatable which is done using: .HasDataTable = True
However I would like to show the values of series as percentages. Currently the value is defined as a Double containing all the values but not the right formatting. Using Format() or FormatPercent() will give the right values but returned in a String. This works for the datatable but not for the chart itself since it doesn't recognize the values anymore.
My question comes down to whether it is possible to show the values as percentages in both the datatable and the chart? Without VBA it is easily done by formatting the data in the cells itself. The problem is that for formatting a String is returned but for the graph Integers or Doubles are needed.
Below is part of the code. If I dim Ratio as String and use FormatPercent() I get the requested formatting but then the values in Ratio ar no longer doubles so it doesn't give the required chart.
Dim Ratio() As Double
Dim labels() As String
ReDim Ratio(1 To Height)
ReDim labels(1 To Height)
For Each Column In sArray
labels(i) = Sheets(DataSheetName).Cells(LabelsRow, Column)
Ratio(i) = Math.Round(Sheets(DataSheetName).Cells(LabelsRow + 3, Column), 2)
i = i + 1
Next Column
Set myChtObj = Sheets(DrawSheetName).ChartObjects.Add(Left:=Left, Width:=Width, Top:=Top, Height:=HeightGraph)
Dim srsNew1 As Series
' Add the chart
With myChtObj.Chart
.ChartArea.Fill.Visible = False
.ChartArea.Border.LineStyle = xlNone
.PlotArea.Format.Fill.Solid
.PlotArea.Format.Fill.Transparency = 1
.HasTitle = True
.ChartTitle.text = Title
.HasLegend = False
.Axes(xlValue).TickLabels.NumberFormat = "0%"
.Axes(xlCategory, xlPrimary).HasTitle = False
'add data table
.HasDataTable = True
' Make Line chart
.ChartType = xlLine
' Add series
Set srsNew1 = .SeriesCollection.NewSeries
With srsNew1
.Values = Ratio
.XValues = labels
.Name = "Ratio"
.Interior.Color = clr3 'RGB(194, 84, 57)
End With
End With

Setting dates on X axis

I'm trying to create a chart in Excel VBA and am having problems getting the X-Axis to display the dates correctly; the code is below:
Function CreateChart()
Dim objChart As Chart
ReDim detached_price(detachedProps.count - 1) As Double
ReDim detached_date(detachedProps.count - 1) As Date
ReDim semi_price(semiProps.count - 1) As Double
ReDim semi_date(semiProps.count - 1) As Date
Dim minDate As Date
Dim maxDate As Date
minDate = Date
Dim detachedCount As Integer
detachedCount = 0
Dim semiCount As Integer
semiCount = 0
Set objChart = Charts.Add
With objChart
.HasTitle = True
.ChartTitle.Characters.Text = "Price Paid"
.ChartType = xlXYScatter
.Location xlLocationAsNewSheet
.Axes(xlCategory, xlPrimary).HasTitle = True
.Axes(xlCategory, xlPrimary).AxisTitle.Characters.Text = "Date"
.Axes(xlCategory, xlPrimary).CategoryType = xlTimeScale
.Axes(xlCategory, xlPrimary).TickLabels.NumberFormat = "yyyy"
.Axes(xlCategory, xlPrimary).MinimumScaleIsAuto = True
.Axes(xlCategory, xlPrimary).MaximumScaleIsAuto = True
For Each prop In properties
Select Case prop.PropertyType
Case "Detached"
detached_price(detachedCount) = prop.Amount
detached_date(detachedCount) = prop.SellDate
detachedCount = detachedCount + 1
Case "Semi-detached"
semi_price(semiCount) = prop.Amount
semi_date(semiCount) = prop.SellDate
semiCount = semiCount + 1
End Select
If prop.SellDate < minDate Then
minDate = prop.SellDate
End If
If prop.SellDate > maxDate Then
maxDate = prop.SellDate
End If
Next
.SeriesCollection.NewSeries
.SeriesCollection(DETACHED).Name = "Detached"
.SeriesCollection(DETACHED).Values = detached_price
.SeriesCollection(DETACHED).XValues = detached_date
.SeriesCollection.NewSeries
.SeriesCollection(SEMI).Name = "Semi-Detached"
.SeriesCollection(SEMI).Values = semi_price
.SeriesCollection(SEMI).XValues = semi_date
End With End Function
The properties variable in the For..Each loop is populated, and fills the arrays correctly.
However, although the Scatter Graph data points are shown, the dates on the axis all show 1900.
I tried adding the lines:
.Axes(xlCategory, xlPrimary).MinimumScale = CDbl(minDate)
.Axes(xlCategory, xlPrimary).MaximumScale = CDbl(maxDate)
Which showed the correct years along the axis, but now all the data points for both series have disappeared.
I've tried a few other things, but it's been purely on a trial and error basis.
The data is as follows
The resulting charts are:
Correct dates, no data points
Incorrect dates, but we have data points
Even though I disagree with their definition of "common" date format :q, I think that #chiliNUT is on to something. There seems to be some problem with the coercion of the date format.
If you change all of your Date type variables to Double or Long, it should work.
For example change
ReDim detached_date(detachedProps.count - 1) As Date
to
ReDim detached_date(detachedProps.count - 1) As Double
or
ReDim detached_date(detachedProps.count - 1) As Long
This way the dates are not converted into String by the XValues method. They are stored as date serial numbers and the axis routine is able to coerce them successfully into the local date format.
Whats happening in your code is the Date types are coerced to String by the XValues method and the axis rendering routine seems to be unable to coerce them back into dates properly.
I don't think it is actually related to the international settings as I tried it using dates like this:
1/01/2013
1/01/2014
1/01/2015
1/01/2016
1/01/2017
1/01/2018
1/01/2019
1/01/2020
1/01/2021
which works in either system.
I think its just a bug in the axis rendering routine where its unable to properly coerce strings into dates.
I would be interested to hear from others more knowledgeable than me.
Also, I'm curious about this:
.SeriesCollection.NewSeries
.SeriesCollection(DETACHED).Name = "Detached"
.SeriesCollection(DETACHED).Values = detached_price
.SeriesCollection(DETACHED).XValues = detached_date
How does it know which series to reference? Is DETACHED a constant you have defined elsewhere?
I would think this would be better:
with objChart.SeriesCollection.NewSeries
.Name = "Detached"
.Values = detached_price
.XValues = detached_date
end with

Excel 2010 Chart axis not shown up

I am converting a EXCEL 2003 application to EXCEL 2010. Data are shown up fine but the axis does not show any more. which function to show the axis with automatic scale?
For Example: If you plot the following series in an Excel Line Chart. [0.22,0.33,0.44,0.55,0.66,0.77,0.88,0.99,1.1,1.21,1.32,1.43,1.54,1.65,1.76,1.87,1.98,2.09,2.2] Excel determines that the y-axis values should be [0,0.5,1,1.5,2,2.5] [How does Excel determine the axis values for charts?1. How to make the y-axis with the automatic values [0,0.5,1,1.5,2,2.5] shown in the chart?
Thanks
Updated with related codes -
With ActiveChart
.SeriesCollection(2).Select
'.SeriesCollection(2).AxisGroup = 2
.HasTitle = True
.ChartTitle.Text = OutputTitle & Chr(10) & ChartTitle2
.Axes(xlValue).HasTitle = True
.Axes(xlValue).AxisTitle.Text = AxisTitle1
.Axes(xlValue).AxisTitle.Font.Bold = False
.HasAxis(Excel.XlAxisType.xlCategory, Excel.XlAxisGroup.xlPrimary) = True
.Export Filename:=ExportFile, FilterName:="GIF"
End with
If I uncomment '.SeriesCollection(2).AxisGroup = 2, I will get the y axis to show but the x axis labels are messed up with mismatch with the Values.
Current chart -
Desired chart with scaled axis shown -
To make sure the axis is on use this:
With xlApp.ActiveChart
.HasAxis(Excel.XlAxisType.xlCategory, Excel.XlAxisGroup.xlPrimary) = True
End With
Range values are automatic unless otherwise specified like this:
' Set Axis Scales
With xlApp.Charts("Chart Name").Axes(2)
.MinimumScale = 100
.MaximumScale = 2000
.MajorUnit = 1000
.MinorUnit = 100
End With
Just to be a little more complete try explicitly addressing each value and category and see if that helps.
With xlApp.ActiveChart
.SeriesCollection(1).XValues = "='sheet name'!R21C4:R46C4"
.SeriesCollection(1).Values = "='sheet name'!R21C5:R46C5"
.SeriesCollection(1).Name = "='series name'"
.SeriesCollection(1).axisgroup = Excel.XlAxisGroup.xlPrimary
.HasAxis(Excel.XlAxisType.xlCategory, Excel.XlAxisGroup.xlPrimary) = True
.HasAxis(Excel.XlAxisType.xlValue, Excel.XlAxisGroup.xlPrimary) = True
.Axes(Excel.XlAxisType.xlCategory, Excel.XlAxisGroup.xlPrimary).HasTitle = True
.Axes(Excel.XlAxisType.xlCategory, Excel.XlAxisGroup.xlPrimary).AxisTitle.Characters.Text = "x axis"
.Axes(Excel.XlAxisType.xlValue, Excel.XlAxisGroup.xlPrimary).HasTitle = True
.Axes(Excel.XlAxisType.xlValue, Excel.XlAxisGroup.xlPrimary).AxisTitle.Characters.Text = "y axis"
End With
I see your axes group is set to 2, are you using dual axis?
Set it like this:
.SeriesCollection(2).axisgroup = Excel.XlAxisGroup.xlPrimary
*Edit*
To set autoscale on the axis:
.Axes(xlValue).MinimumScaleIsAuto = True
.Axes(xlValue).MaximumScaleIsAuto = True
.Axes(xlValue).MinorUnitIsAuto = True
.Axes().MajorUnitIsAuto = True
.Axes().DisplayUnit = xlHundreds

Is there a way to put bounds on Goal Seek? If not, how would you go about this?

I'm trying to minimize the value of the sum of the residuals squared by varying the value of De, which is found in F1. I want the values of CFL Calculated to be as close as possible to the values of CFL Measured. The smaller the sum of those residuals squared, the better the fit! After asking stackoverflow for some advice, I decided to use Goal Seek to minimize the sum of the residuals squared to get as close to zero as possible by varying the value of De, which I want to find the most ideal value of.
I got this program to run perfectly, or so I thought... I found out that instead of summing every single residuals using =SUM(D2:D14), I accidentally used =SUM(D2,D14). So I was only summing up the first and last numbers.
Now that I'm trying to sum every residual squared up, I'm getting these crazy errors, and an insane value for De.
I know that the value of De has to be greater than zero, and less than one. how can I use these bounds to keep this goal seek focused within a certain range? The answer for De in this case is about .012, if that helps. I keep getting the error #NUM! in all of the residual cells. Is this because of overflow issues?
If you've concluded that using Goal Seek to minimize these sums by finding the most ideal value of De will not work, how would you go about it? Are there any other solvers I could use?
Here is the code:
Option Explicit
Dim Counter As Long
Dim DeSimpleFinal As Double
Dim simpletime As Variant
Dim Tracker As Double
Dim StepAmount As Double
Dim Volume As Double
Dim SurfArea As Double
Dim pi As Double
Dim FinalTime As Variant
Dim i As Variant
Sub SimpleDeCalculationNEW()
'This is so you can have the data and the table I'm working with!
Counter = 13
Volume = 12.271846
SurfArea = 19.634954
pi = 4 * Atn(1)
Range("A1") = "Time(days)"
Range("B1") = "CFL(measured)"
Range("A2").Value = 0.083
Range("A3").Value = 0.292
Range("A4").Value = 1
Range("A5").Value = 2
Range("A6").Value = 3
Range("A7").Value = 4
Range("A8").Value = 5
Range("A9").Value = 6
Range("A10").Value = 7
Range("A11").Value = 8
Range("A12").Value = 9
Range("A13").Value = 10
Range("A14").Value = 11
Range("B2").Value = 0.0612
Range("B3").Value = 0.119
Range("B4").Value = 0.223
Range("B5").Value = 0.306
Range("B6").Value = 0.361
Range("B7").Value = 0.401
Range("B8").Value = 0.435
Range("B9").Value = 0.459
Range("B10").Value = 0.484
Range("B11").Value = 0.505
Range("B12").Value = 0.523
Range("B13").Value = 0.539
Range("B14").Value = 0.554
Range("H2").Value = Volume
Range("H1").Value = SurfArea
Range("C1") = "CFL Calculated"
Range("D1") = "Residual Squared"
Range("E1") = "De value"
Range("F1").Value = 0.1
'Inserting Equations
Range("C2") = "=((2 * $H$1) / $H$2) * SQRT(($F$1 * A2) / PI())"
Range("C2").Select
Selection.AutoFill Destination:=Range("C2:C" & Counter + 1), Type:=xlFillDefault
Range("D2") = "=((ABS(B2-C2))^2)"
Range("D2").Select
Selection.AutoFill Destination:=Range("D2:D" & Counter + 1), Type:=xlFillDefault
'Summing up the residuals squared
Range("D" & Counter + 2) = "=Sum(D2: D" & Counter + 1 & ")"
'Goal Seek
Range("D" & Counter + 2).GoalSeek Goal:=0, ChangingCell:=Range("F1")
Columns("A:Z").EntireColumn.EntireColumn.AutoFit
DeSimpleFinal = Range("F1")
MsgBox ("The Final Value for DeSimple is: " & DeSimpleFinal)
End Sub
You're getting NUM errors because the value of F1 is going negative in your current solution -- and you are trying to take the square root of F1 in one of your expressions.
Also, goal seek is, in this instance, incredibly sensitive to the particular initial starting "guess" for F1 that you are using. This will be evident if you vary the F1 initial value by a little bit on either side of the 0.1 you are using now. There are, in fact, large regions of instability in the goal seek solution, depending on the F1 value:
As you brought up in your question, you are more likely to get a useable result if you can set constraints on the possible inputs to your solution search. Excel comes with an add-in called Solver that allows that, as well as offers several different search methods. Solver is not loaded automatically when you first start Excel, but loading it is easy, as explained here.
You ask for other solvers. For alternatives and a bit of theory to help understand what's going on, have a look at Numerical Recipes (online books here). Chapter 10 deals with this. It includes ready-made code samples if you want to try something different than GoalSeek or the Solver add-in. Of course the code is in Fortran/C/C++ but these are readily translated into VBA (I've done this many times).
The goalseek function uses a dichotomy algorithm which can be coded like this:
Sub dicho(ByRef target As Range, ByRef modif As Range, ByVal targetvalue As Double, ByVal a As Double, ByVal b As Double)
Dim i As Integer
Dim imax As Integer
Dim eps As Double
eps = 0.01
imax = 10
i = 0
While Abs(target.Value - targetvalue) / Abs(targetvalue) > eps And i < imax
modif.Value = (a + b) / 2
If target.Value - targetvalue > 0 Then
a = (a + b) / 2
Else
b = (a + b) / 2
End If
i = i + 1
Wend
End Sub
Where a and b are you bounds.