Line Chart Graphical Alert - c#

I currently have a line graph in my C# program, and I have a min and max variable. If any the graph ever exceeds the max, or goes below the min, is there any built in way of displaying on the graph (such as a dot at the point) that the limit was passed, and display the x/y values for that point?
int max = 2000;
int min = 2000;
for (int i = 0; i < dgvLoadedValues.RowCount - 1; i++)
{
DateTime x = Convert.ToDateTime(dgvLoadedValues.Rows[i].Cells[0].Value.ToString());
try
{
float y = float.Parse(dgvLoadedValues.Rows[i].Cells[e.ColumnIndex].Value.ToString());
chart1.Series["Series1"].Points.AddXY(x, y);
}
catch
{
Console.WriteLine("Unable to plot point");
}
}
Code above simply shows values taken from a datagridview and displaying it into a line graph
Thank you

Unfortunately there seems to be no way to define such an automatic alert.
But as you know just when the DataPoints are added or bound you can set a Marker where necessary.
Here is a loop that does it after the fact in one go, but of course you can just as well set the markers as you add the points..:
foreach (DataPoint dp in chart1.Series[0].Points)
{
if (dp.YValues[0] < max && dp.YValues[0] > min ) continue;
dp.MarkerStyle = MarkerStyle.Circle;
dp.MarkerColor = Color.Red;
}
Or in your case:
try
{
float y = float.Parse(dgvLoadedValues.Rows[i].Cells[e.ColumnIndex].Value.ToString());
int i = chart1.Series["Series1"].Points.AddXY(x, y);
if (y < min || y > max)
{
chart1.Series["Series1"].Points[i].MarkerStyle = MarkerStyle.Circle;
chart1.Series["Series1"].Points[i].MarkerColor = Color.Red;
}
}
To clear a marker you can set its MarkerStyle = MarkerStyle.None.
Of course you could easily give the min and max points different colors..
Here is an example with the simple circle style, but there are others including images..:
To add the values in a label use a format like this:
dp.Label = "(#VALX{0.0} / #VAL{0.0})" ;

Related

How to get Pixel Position from DateTime Value on X-Axis?

I'm trying to retrieve the pixel position on the chart of a DataPoint using its X Value (DateTime).
I'm using this code after the Chart has been painted but I get a very large number:
DateTime date = ... ; // DateTime of the DataPoint, I verified the date is correct and the chart contains it.
var pixelsPosition = ChartAreas[0].AxisX.ValueToPixelPosition(date.ToOADate());
// Here pixelsPosition is a very large number, above 600 000.
These are the Chart settings:
Series[0].XValueType = ChartValueType.DateTime;
Series[0].IsXValueIndexed = true;
The Chart has around 2000 Points.
I'm using the same code to retrieve a pixel position of a DataPoint using the Y Value and it works. I'm sure it's a stupid problem but I can't figure it out.
Working code for the Y-Axis
double yValue = 100;
double pixPositionY = ChartAreas[0].AxisY.ValueToPixelPosition(yValue); // THIS WORKS
What am I doing wrong?
EDIT:
I read that the method ValueToPixelPosition works only on Paint Events, I've tried to execute it on the Paint and PrePaint events but I get the same error.
I have found the solution to my problem:
DateTime date;
int pixelPositionX = 0;
var point = Series[0].Points.Where(X => X.XValue == date.ToOADate()).FirstOrDefault();
var index = (point != null) ? Series[0].Points.IndexOf(point) : -1;
if (index > -1)
pixelPositionX = (int)ChartAreas[0].AxisX.ValueToPixelPosition(index + 1);
That gives me the correct position of the DataPoint on the screen. ValueToPixelPosition works if I pass the dataPoint index as parameter rather than the datetime value converted to OADate. I'm not sure if this is the right solution but it's working for me.
I have the same problem, but your solution is surely wrong. ValueToPixelPosition() requires a double value and not an index. Apart from that your workaround is very slow because it must search through all DateTime values on X axis.
To find out how Microsoft generates these strange X values I used ILSpy to disassemble System.Windows.Forms.DataVisualization.ni.dll which can be found in C:\Windows\assembly\....
Here is the Microsoft code which converts DateTime into double:
// System.Windows.Forms.DataVisualization.Charting.DataPoint
public void SetValueXY(object xValue, params object[] yValue)
{
.......
else if (type == typeof(DateTime))
{
_xValue = ((DateTime)xValue).ToOADate();
}
........
if (series != null && xValue is DateTime)
{
if (series.XValueType == ChartValueType.Date)
{
DateTime dateTime = new DateTime(((DateTime)xValue).Year, ((DateTime)xValue).Month, ((DateTime)xValue).Day, 0, 0, 0, 0);
_xValue = dateTime.ToOADate();
}
else if (series.XValueType == ChartValueType.Time)
{
DateTime dateTime2 = new DateTime(1899, 12, 30, ((DateTime)xValue).Hour, ((DateTime)xValue).Minute, ((DateTime)xValue).Second, ((DateTime)xValue).Millisecond);
_xValue = dateTime2.ToOADate();
}
}
.....
If you use ChartValueType.Time the double value on X axis is between 0.0 (for 00:00:00.000) and 1.0 (for 23:59:59.999)
I don't like the Microsoft code using a date from 1899.
This can be done easier:
int s32_Millis = (((k_Time.Hour*60) +k_Time.Minute)*60 +k_Time.Second)*1000 +k_Time.Millisecond;
double d_OaTime = (double)s32_Millis / (24*60*60*1000);
double d_PixelPosX = ChartAreas[0].AxisX.ValueToPixelPosition(d_OaTime);

Convert seconds to hhh:mm:ss in a chart

I have a MsSql database which calculates the timespan between two dates in seconds. That works fine. I use this column afterwards in C# and write them in an array.
This array is the input for a chart later on.
So far this works well, but I cannot find a way to display the seconds in a format like hhh:mm:ss as the timespan can be greater than 24h.
I tried ChartArea.AxisY.LabelStyle.Format = "hhmmss"; but it does not work at all.
Does anybody has an idea how I could do that?
EDIT:
I add the data this way:
chart2.Series.Clear();
chart2.ChartAreas.Clear();
Series BoxPlotSeries = new Series();
ChartArea ChartArea2 = new ChartArea();
ChartArea ChartArea3 = new ChartArea();
chart2.ChartAreas.Add(ChartArea2);
chart2.ChartAreas.Add(ChartArea3);
ChartArea2.Name = "Data Chart Area";
ChartArea3.Name = "BoxPlotArea";
BoxPlotSeries.Name = "BoxPlotSeries";
BoxPlotSeries.ChartType = SeriesChartType.BoxPlot;
BoxPlotSeries.ChartArea = "BoxPlotArea";
chart2.Series.Add(BoxPlotSeries);
Series Input1 = new Series();
Input1.Name = "Input1";
Input1.ChartType = SeriesChartType.Point;
Input1.ChartArea = "Data Chart Area";
chart2.Series.Add(Input1);
chart2.Series["Input1"].Points.DataBindY(InputArray);
chart2.ChartAreas["BoxPlotArea"].AxisX.CustomLabels.Add(2, 0.0, "BoxPlot1");
chart2.Series["BoxPlotSeries"]["BoxPlotSeries"] = "Input1";
chart2.Series["BoxPlotSeries"]["BoxPlotShowMedian"] = "true";
chart2.Series["BoxPlotSeries"]["BoxPlotShowUnusualValues"] = "false";
chart2.Series["BoxPlotSeries"]["PointWidth"] = "0.5";
chart2.Series["BoxPlotSeries"].IsValueShownAsLabel = false;
ChartArea2.Visible = false;
ChartArea3.BackColor = Color.FromArgb(224,224,224);
//I tried to format it this way but it didn't work
//ChartArea3.AxisY.LabelStyle.Format = "{0:HHHmmss}";
chart2.ChartAreas["BoxPlotArea"].AxisX.LabelStyle.Angle = -90;
EDIT2:
And here's how I populate the input array
int[] InputArray = new int[1000000];
int c = 0;
con.Open();
dr = cmd.ExecuteReader();
if (dr.HasRows)
{
while (dr.Read())
{
int n;
if (int.TryParse(dr[0].ToString(),out n) == true)
{
InputArray[c] = Convert.ToInt32(dr[0].ToString());
c++;
}
}
}
if (c == 0) { c = 1; }
Array.Resize(ref InputArray, c - 1);
EDIT 3:
The Boxplot should look like this in the end:
In Excel the format to display hours greater than 24 is called "[h]:mm:ss;#"
EDIT4:
Thanks to #TAW I nearly managed to solve my problem. I made some adjustments to his solution and came up with this:
In the chart code block:
The Value "max" is set before.
ChartArea3.AxisY.MajorTickMark.Interval = addCustomLabels(ChartArea3, BoxPlotSeries, 60 * 60, max);
int addCustomLabels(ChartArea ca, Series series, int interval, int max)
{
int tickNo = 0;
ca.AxisY.CustomLabels.Clear();
if(max / interval > 10)
{
interval = (max / 10) - (max / 10) % (60*30);
tickNo = (max / 10) - (max / 10) % (60*30);
}
if (max / interval <= 2 )
{
interval = (max / 4) - (max / 4) % (60 * 15);
tickNo = (max / 4) - (max / 4) % (60 * 15);
}
for (int i = 0; i < max; i += interval)
{
CustomLabel cl = new CustomLabel();
cl.FromPosition = i - interval / 2;
cl.ToPosition = i + interval / 2;
cl.Text = hhh_mm_ss(i);
ca.AxisY.CustomLabels.Add(cl);
}
return tickNo;
}
My problem is now, that sometimes no axis lable (apart from 0:00) is shown even when the code runs through it without any problems.
Has anybody and idea what could be wrong?
Your task involves two parts:
displaying seconds in the hhh:mm:ss format
putting them as labels on the y-axis
There is no suitable date-time formatting string for this in c#, so we can't make use of the built-in automatic labels and their formatting.
There also no way to use expressions that call a function on the automatic labels, unfortunately.
So we can't use those.
Instead we will have to add CustomLabels. This is not very hard but does take a few steps..
But let's start with a function that converts an int to the hhh:mm:ss string we want; this should do the job:
string hhh_mm_ss(int seconds)
{
int sec = seconds % 60;
int min = ((seconds - sec)/60) % 60;
int hhh = (seconds - sec - 60 * min) / 3600;
return hhh > 0 ? string.Format("{2}:{1:00}:{0:00}", sec, min, hhh)
: min + ":" + sec.ToString("00");
}
Maybe it can be optimized, but for our purpose it'll do.
Next we need to create the CustomLabels. They will replace the normal axis labels and we need to add them in a separate loop over the data after each binding.
One special thing about them is their positioning. Which is smack between two values we need to give them: the FromPosition and ToPosition, both in the unit of the axis-values.
Another difference to normal, automatic Labels is that it is up to us to create as many or few of them as we need..
This function tries to create a number that will go up to the maximum y-value and space the CustomLabels at a given interval:
void addCustomLabels(ChartArea ca, Series series, int interval)
{
// we get the maximum form the 1st y-value
int max = (int)series.Points.Select(x => x.YValues[0]).Max();
// we delete any CLs we have
ca.AxisY.CustomLabels.Clear();
// now we add new custom labels
for (int i = 0; i < max; i += interval)
{
CustomLabel cl = new CustomLabel();
cl.FromPosition = i - interval / 2;
cl.ToPosition = i + interval / 2;
cl.Text = hhh_mm_ss(i);
ca.AxisY.CustomLabels.Add(cl);
}
}
The first parameters to call this are obvious; the last one however is tricky:
You need to decide to interval you want your labels to have. It will depend on various details of your chart:
the range of values
the size of the chart area
the size of the font of the axis
I didn't set any special Font in the function; CustomLabels use the same Font as normal axis labels, i.e. AxisY.LabelStyle.Font.
For my screenshot I prepared the chart like this:
ca.AxisX.Minimum = 0;
ca.AxisY.MajorTickMark.Interval = 60 * 60; // one tick per hour
addCustomLabels(ca, s, 60 * 30); // one label every 30 minutes
I have also added DataPoint Labels for testing to show the values..:
series.Points[p].Label = hhh_mm_ss((int)y) + "\n" + y;
Here is the result:
UPDATE: This answer may be quite useful for other readers, but it pretty much misses the OP's issues. I'll leave it as it stands, but it will not help in creating specially formatted y-axis labels..
Most Chart problems stem from invalid or useless x-values. The following discussion tries to help avoiding or getting around them..
A number is a number and you can't simply display it as a DateTime, or for that matter a TimeSpan.
So you need to add the X-Values as either DateTime or as double that contain values that can be converted to DateTime. The fomer is what I prefer..
So instead of adding the seconds directly add them as offsets from a given DateTime:
Change something like this
series.Points.AddXY(sec, yValues);
To this:
var dt = new DateTime(0).AddSeconds(sec);
series.Points.AddXY(dt, yValues);
Now you can use the date and time formatting strings as needed..:
chartArea.AxisX.LabelStyle.Format = "{mm:ss}";
You could also add them as doubles that actually are calculated from DateTimes via the ToOADate:
series.Points.AddXY(dt.ToOADate(), yValues);
But now you will have to set the ChartValueType.DateTime and probably also AxisX.IntervalType and AxisX.Interval to make sure the chart gets the formatting right..:
s.XValueType = ChartValueType.DateTime;
ca.AxisX.Interval = 5;
ca.AxisX.IntervalType = DateTimeIntervalType.Seconds;
ca.AxisX.LabelStyle.Format = "{mm:ss}";
Pick values that suit your data!
Note that the problem with your original code is that the X-Values internally always are doubles, but the seconds are not integer values in them but fractional parts; so you need some kind of calculation. That's what ToOADate does. Here is a short test that shows what one second actually does amount to as a OADate double :
Best add the X-Values as DateTimes so all further processing can rely on the type..
Update I just saw that you have finally added the real code to your question and that is uses Points.DataBindY. This will not create meaningful X-Values, I'm afraid. Try to switch to Points.DataBindXY! Of course the X-Values you bind to also need to follow the rules I have explained above..!
You can do a loop over your array and convert the numbers like I shown above; here is a simple example:
int[] seconds = new int[5] { 1, 3, 88, 123, 3333 };
double[] oaSeconds = seconds.Select(x => new DateTime(0).AddSeconds(x).ToOADate())
.ToArray();
If you are trying to show more than 2 digits of hour I think this should work for you
//yourTimeSpan is the TimeSpan that you already have
var hoursDouble = Math.Floor(yourTimeSpan.TotalHours);
string hours;
string minutes;
string seconds;
//check hours
if(hoursDouble < 10)
{
hours = string.Format("0{0}", hoursDouble);
}
else
{
hours = hoursDouble.ToString();
}
//check minutes
if (yourTimeSpan.Minutes < 10)
{
minutes = string.Format("0{0}", yourTimeSpan.Minutes);
}
else
{
minutes = yourTimeSpan.Minutes.ToString();
}
//check seconds
if (yourTimeSpan.Seconds < 10)
{
seconds = string.Format("0{0}", yourTimeSpan.Seconds);
}
else
{
seconds = yourTimeSpan.Seconds.ToString();
}
string formattedSpan = String.Format("{0}:{1}:{2}", hours, minutes, seconds);
Update: I think this should solve the problem you were seeing with single digit numbers

Point Classification in a set of Bounding Boxes

I have a set of bounding boxes(rectangular) in a 3D space. The bounds of each box are computed and stored in a dictionary named "RegionBounds". Also, a set of points are populated in a List named "PointsToCategorize" Given a point(x,y,z) coordinates from the List populated and a bounding box to be checked in, i can check if the point is inside the box or not. The problem is, this is a big dataset. The number of points to be checked are like 1000 and the no of bounding boxes are like 250-300. So, if i loop through each bounding box for each given point; the total time it takes is like 5-6 minutes. Is there any efficient method that would do the process quicker ? If possible, a small code to do so would be great
public struct iBounds {
public double x1, x2;
public double y1, y2;
public double z1, z2;
}
public struct iPoint {
public double x,y,z
}
Dictionary<String, iBounds> RegionBounds = new Dictionary<String, iBounds>();
List<iPoint> PointsToCategorize = new List<iPoint>();
int no_of_bounding_boxes = 300;
int no_of_points_to_categorize = 1000;
for (int i = 1; i <= no_of_bounding_boxes; i++)
{
String boundingBoxName = "bound_" + i;
iBounds boundingBox = new iBounds
{
x1 = Computed By Some Other method and Formulas,
x2 = Computed By Some Other method and Formulas,
y1 = Computed By Some Other method and Formulas,
y2 = Computed By Some Other method and Formulas,
z1 = Computed By Some Other method and Formulas,
z2 = Computed By Some Other method and Formulas
};
RegionBounds.Add(boundingBoxName, boundingBox);
}
////////////Start of Output section /////////////////////////
for(int i= 1; i < = PointsToCategorize.Count; i++){
foreach(var pair in RegionBounds)
{
String myboxNmame = pair.Key;
iBounds myboxBounds = pair.Value;
Console.WriteLine(PointInside(PointsToCategorize[i],myboxBounds).ToString());
}
}
////////////// End of Output section //////////////////
private bool PointInside(iPoint mypoint, iBounds boxToBeCheckedIn)
{
if (mypoint.x > boxToBeCheckedIn.x1) && (mypoint.x < boxToBeCheckedIn.x2){
if (mypoint.y > boxToBeCheckedIn.y1) && (mypoint.y < boxToBeCheckedIn.y2){
if (mypoint.z > boxToBeCheckedIn.z1) && (mypoint.z < boxToBeCheckedIn.z2){
return true;
}
}
}else{
return false;
}
}
You may want to use a OcTree or a kD-tree data structure, which is way more efficient than iterating through all the boxes.
See also this article at the section 2-D orthogonal range searching, it has a very good resume of available techniques and algorithms, which are easily extendable to 3D

Creating a graph with relative distance (C#)

I have the following problem. I create a chart with migradoc in c#.
Suppose I have the following points for my xAxis:
20.4, 20.6, 30.6, 100.4, 200.3
The problem is that it sets every xpoint in the series on an equal distance in the chart.
While what I need is a graph who sets the xpoints on a relative distance. For example, the distance between points 20.6 and 30.6 needs to be way smaller than the distance between 30.6 and 100.4. (The points always differ, as do the number of points)
One way to make the distance good is to add extra points between the existing points. For example the first step is 0.2 extra, the second step is 10.0 extra. So I want to add for example 50 extra points between this step, so that the distance is relative the same.
This is the only thing I can come up with, can somebody give me some advice how to accomplish this? (Or another possible solution?)
This method worked out for me. I first made the distances relative:
Int64[] relAfstand = new Int64[afstand.Count()];
for(int i = 0; i < afstand.Count(); i++){
double tussenRel = Convert.ToDouble(afstand[i]);
double eindRel = Convert.ToDouble(afstand[afstand.Count()-1]);
double beginRel = Convert.ToDouble(afstand[0]);
double Rel = (((eindRel - beginRel) - (eindRel - tussenRel)) / (eindRel - beginRel));
relAfstand[i] = Convert.ToInt64((Rel)*100);
}
Then I converted the data to scale with relative with the same factor as the distances:
List<double> ConvertedData = new List<double>();
int c = 0;
int c2 = 1;
double steps = 0;
bool calcSteps = false;
bool calcDistance = false;
for (int i = 0; i < 100; i++) {
if (calcDistance == false) {
distance.Add(i);
}
if (relAfstand[c] == i) {
ConvertedData.Add(data[c]);
calcSteps = false;
c2 = 1;
c++;
}else {
if (calcSteps == false) {
steps = ((data[c] - data[c-1])/(relAfstand[c] - relAfstand[c-1]));
calcSteps = true;
}
ConvertedData.Add(data[c-1] + (steps * c2));
c2++;
}
}
calcDistance = true;
Probably not the best workaround, but it works. Since the percentages can come close together I scale both now with around 200-300 instead of 100.

Problems with double array?

I am required to create a program which reads in data from a .cvs file, and use these (x, y and z) values for a series of calculations.
I read in the file as a string, and then split this into 3 smaller strings for x, y and z.
The x, y and z coordinates represents the x and y coordinates of the contours of a lake, and the depth (z).
One of the calculations which I have to do, is to calculate the surface area of the lake, using the formula (x[i]*y[i+1])-(x[i+1]*y[i]), where z(depth) = 0.
I can get my code to run perfectly, up until the x[i+1] and y[i+1], where it keeps giving me a value of 0.
Can someone please tell me how to fix this?
Here is my code;
{
string[] ss = File.ReadAllLines(#"C:File.csv");
for (int i = 1; i < ss.Length; i++)
{
string[] valuesAsString = ss[i].Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
double[] X = new double[valuesAsString.Length];
double[] Y = new double[valuesAsString.Length];
double[] Z = new double[valuesAsString.Length];
for (int n = 0; n < 1; n++)
{
X[n] = double.Parse(valuesAsString[0]);
Y[n] = double.Parse(valuesAsString[1]);
}
do
{
double SurfaceArea = (X[n] * Y[n + 1]) - (X[n + 1] * Y[n]);
Console.WriteLine(SurfaceArea);
}
while (Z[n] == 0);
}
}
Ok, im not sure if i got it right, so you if you would take a look to what i did and tell me if its of any help.
After reviewng it a little i came up with the following:
A class for the values
public class ValueXyz
{
public double X { get; set; }
public double Y { get; set; }
public int Z { get; set; }
}
A class to manange the calculation:
public class SurfaceCalculator
{
private ValueXyz[] _valuesXyz;
private double _surface;
private readonly string _textWithValues;
public SurfaceCalculator(string textWithValues)
{
_textWithValues = textWithValues;
SetValuesToCalculate();
}
public double Surface
{
get { return _surface; }
}
public void CalculateSurface()
{
for (var i = 0; i < _valuesXyz.Length; i++)
{
if (_valuesXyz[i].Z == 0)
_surface = (_valuesXyz[i].X*_valuesXyz[i + 1].Y) - (_valuesXyz[i + 1].X*_valuesXyz[i].Y);
}
}
private void SetValuesToCalculate()
{
var valuesXyz = _textWithValues.Split(' ');
_valuesXyz = valuesXyz.Select(item => new ValueXyz
{
X = Convert.ToDouble(item.Split(',')[0]),
Y = Convert.ToDouble(item.Split(',')[1]),
Z = Convert.ToInt32(item.Split(',')[2])
}).ToArray();
}
}
So now your client code could do somethin like:
[TestMethod]
public void TestSurfaceCalculatorGetsAValue()
{
//var textWithValues = File.ReadAllText(#"C:File.csv");
var textWithValues = "424.26,424.26,0 589.43,231.46,0 720.81,14.22,1";
var calculator = new SurfaceCalculator(textWithValues);
calculator.CalculateSurface();
Assert.IsNotNull(calculator.Surface);
}
I'm not very sure i got the idea correct of how to implement the formula, but i just wanted to expose an alternative you can use, you can never have to many ways of doing one thing :).
Cheers.
By the way part of the intent i had, was not tying up your funcionality to the csv in case your source for the text in the future would change.
Step through your code in the debugger. Pay special attention to tbe behavior of the line
for (int n = 0; n < 1; n++)
This loop will execute how many times? What will the value of n be during each iteration through the loop?
Well, one thing i noticed is when you're setting your X, Y, Z vars, you're setting it to the Length of the array object instead of it's value - is that intentional?
Put a debug break on the line with:
double SurfaceArea = (X[n] * Y[n + 1]) - (X[n + 1] * Y[n]);
and check the datatype of "X", "Y" and "Z"
I've had problems in the past where it tries to calculate them as strings (because it took it out of the data source as strings). I ended up fixing it by adding CInt() to each of the variables (or Convert.ToInt32();).
Hope this helps.
As this looks like it might be a homework problem, I am trying not to give a direct solution in my answer, but I see a number of questionable parts of your code that you should examine.
Why are X, Y, Z arrays? You are creating a new array each time through the outer loop, setting the length of the array to the number of elements in the line, then only assigning a value to one element of X and Y, and never assigning Z to anything.
As phoog suggests in his answer, what is the purpose of: for (int n = 0; n < 1; n++)?
What are you trying to accomplish with the do-while loop? As it has been mentioned in the comments by Mr Skeet, X[n], Y[n], Z[n] don't exist because n does not exist outside of the loop it is declared for. Even if it did exist Z[n] will always be zero because you never assign anything to the Z array after it is initialized, so the do-while loop will run forever.

Categories

Resources